Skip to content

Commit

Permalink
Merge pull request #51 from cs50/develop
Browse files Browse the repository at this point in the history
Deploy check50 v2.0.0
  • Loading branch information
brianyu28 authored Jul 31, 2017
2 parents 163fe9b + 908dacf commit d9cb978
Show file tree
Hide file tree
Showing 20 changed files with 870 additions and 508 deletions.
9 changes: 5 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.c
__pycache__/*
checks/__pycache__/*
checks/mario/__pycache__/*
*.pyc
*.egg-info
dist
build
__pycache__/
31 changes: 31 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
language: python
python:
- '2.7'
- '3.4'
branches:
except: "/^v\\d/"
install: true
script:
- pip install .
- check50 --help
jobs:
include:
- stage: deploy
python: '3.4'
install: true
script: true
deploy:
- provider: script
script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\",
\"target_commitish\": \"$TRAVIS_COMMIT\", \"name\": \"v$(python setup.py --version)\"
}" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases'
on:
branch: master
- provider: pypi
user: "$PYPI_USERNAME"
password: "$PYPI_PASSWORD"
on:
branch: master
notifications:
slack:
secure: DHyiOOI74L9aIi2raGNpsGpyNC5STBci9HvL22l+ujl0h4rcT3fg50Gj9tTtvlKsJwe+gGMx8nXwZZRO/d6uuBlr5Sez3DcBaQbC5YRJQDAxorL4R8ay+2hGtU00dMzH4mtfJwiH9lISfXf+L5nXDqisckN6KtcwHh8hRrK6wjU35D035aIKoWvKcZYA5hhc9uE6D+LV+mVMr7AK9mUDUl3kpzbc9KChRgjyP893rpEKInVbQeZtpT96v0/yyM3ef1l+x9trw7dHZWy2OfXcP4VG6SFywCls8vt4fAeIm50mpHOr11R6QScRmjB562xVt3BU1fwKjSv57nZ/N7IiNAQ97WY8LAanHQQosfESahHbXYFQ0RpPVXfdwEhQetlUuh32w5uJNqZ0Nx/CjoMO4cToTeNsYCeCIIFNKjlFLfbfuJsGqWxb3g9EgtRAYhVQSok1fNPJNbfx3SfA5ZhCFv1t8HGZADXrN0wRPblygVSyG1ELUb3KWbdQ5XtE5S+Dr2r2YVcbrVgc6OQ60g6CowItzcz91K2DFbWyUQoSHMH6iLrqa3hOBRCKsnv4YBRwn0O5B4+KmfuJBz+xj1qmlUw7YAWMmu2PUoM/X9NS+NTzE85IyQY2/KTWeqK02ixyKPqj8+vqsC8mLEz9PHJjQcaBWaGIQUNTDNIKHcUtDVA=
179 changes: 179 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,180 @@
# check50

## Running `check50`

Run `check50` via:

```
python check50.py identifier [files to check]
```

For example, to check `greedy`, assuming you have a file called `greedy.c` in the same directory as `check50.py`, run:

```
python check50.py 2017/x/greedy greedy.c
```

Identifier names are given by the filenames in the `checks` directory, with subdirectories specified by the `/` symbol.
To check multiple files, separate each filename with a space. Alternatively, not specifying any files will by default use all files
in the current directory (TODO: change this to use the `exclude` files).

## Contributing to `check50`

To contribute to `check50`:

* Fork the `cs50/check50` repository (button in the upper-right corner of the window)
* Clone the forked repository, committing and pushing new changes.
* Submit a pull request to `cs50/check50` from the forked repository.

### Check File Structure

All checks are located in a subdirectory of the `checks` directory.
Each problem which requires checks has its own `checks.py` file.
`checks.py` should contain a single class, named after the problem, which is a sublcass of `check50.Checks`.
Individual checks are methods defined within the class.
Check functions must be decorated with the `@check` decorator, and must have a one-line docstring which
describes what the method is checking. The `@check` decorator may optionally take a single parameter:
the name of another check method which must have passed before the decorated check can run. Before a
check runs, all files from the dependency check's working directory are copied into the decorated check's
working directory.

### Check Successes and Failures

To indicate that a check has failed, the exception `check50.Error` must be raised.
This can be raised directly by the check method, or indirectly via a method of `check50.Checks`
which is called by the check method.

`check50.Error` takes a single parameter. If the parameter is a string, it represents the
reason for the check failure. If the parameter is a tuple of `(observed, desired)`, then it
represents that `observed` was the observed output, whereas `desired` was the desired output.

If a check method finishes without raising `check50.Error`, then the test is assumed to have passed.

### `rationale`, `helpers`, and `log`

In addition to keeping track of the success and failure of tests,
`check50` maintains three other per-test-case values that store information
about what took place during a given check.

`self.rationale` is a string, representing the reason that the check failed, and is set by `check50.Error`.

`self.helpers` is a list of strings, representing zero or more lines of suggestions as to why a check
may have failed. Check functions may optionally choose to add to `self.helpers` if they can guess why
a particular `check50.Error` was raised.

`self.log` is a list of strings, representing what steps took place during the check function.
Check functions may optionally add to `self.log`, and methods in `check50.Checks` will also
add to `self.log`.

### Sample Check and Explanation

```
@check("compiles")
def test5(self):
"""5 minutes equals 60 bottles."""
self.spawn("./water").stdin("5").stdout("^.*60.*$", 60).exit(0)
```

The `@check` decorator indicates that this is a check method. The parameter `"compiles"`
means that unless the `compiles` check passed, this check (`test5`) will not run. It also
means that before this test is run, the state of the working directory after the `"compiles"`
check ran will be copied into `test5`'s working directory (including the compiled `water`
binary).

The docstring `"5 minutes equal 60 bottles."` is a description of what this check
method is testing.

First, `self.spawn("./water")` is called, which executes `./water` as a child process.
Then, the string `"5"` is passed into the program via the `.stdin()` method.

The `.stdout()` method takes two parameters: the first is a regular expression indicating
the desired output of the program. The second parameter is a human-friendly representation
of the desired output, which will be displayed to students if the check fails. Here,
we assert that the output of `./water` should match the regular expression `"^.*60.*$"`.
If this is not the case, a `check50.Error` will be raised.

Finally, we assert that the exit status of the program is `0`.

### `check50.Checks`

#### `checkfile(self, filename)`

Takes a relative path to a file located in the directory containing `check.py`
and returns its contents as a string.

#### `diff(self, f1, f2)`

Takes two files, `f1` and `f2`, which can be strings or `check50.File` types.
If the filemaes are strings, they are interpreted as relative paths in the
temporary check directory. If the filenames are of type `check50.File`,
then type are considered relative paths in the directory containing
`check.py`.

Compares the two files, and outputs `0` if the two files are the same,
and nonzero otherwise.

#### `exists(self, filename)`

Asserts that `filename` exists, and raises a `check50.Error` otherwise.

#### `hash(self, filename)`

Hashes `filename` using SHA-256, and returns the hash of the file.

#### `spawn(self, cmd, env)`

Spawns the child process running `cmd` with the environment `env`.
Returns a `check50.Child`, which is a wrapper on a `pexpect` child process.

#### `include(self, path)`

Includes `path`, a relative path in the directory containing `check.py`
into the temporary check directory.

#### `append_code(self, filename, codefile)`

Appends the contents of `codefile` to the end of `filename` in the
temporary directory. Useful for replacing a Python function during
testing.

#### `fail(self, rationale)`

Fails the test with the given `rationale`.

### `check50.Child`

#### `stdin(self, line, prompt=True)`

Passes `line` as input to the child process.
The `prompt` variable, default `True`, determines whether
`check50` should wait for a nonzero-character prompt to appear
before passing in `line`.

#### `stdout(self, output=None, str_output=None)`

Asserts that, at program's termination, the
child process produces output that matches the regex `output`,
and raises a `check50.Error` otherwise. If the error is raised,
the `rationale` includes that the expected output was `str_output`.

If `output` is `None` (i.e. by default, if no parameters are passed
into the `stdout` function), then the function returns a string
representing the program's output.

#### `reject(self)`

Asserts that the child process should have rejected an input
and prompted for another input. Typically called after a call to `.stdin()`.
If the child process did not prompt for another input, then a
`check50.Error` will be raised.

#### `exit(self, code=None)`

If `code` is not `None`, then `exit` asserts that the program terminated
with exit code `code`, and raises a `check50.Error` otherwise.

If `code` is `None`, then `exit` returns the exit code of the program.

#### `kill(self)`

Terminates the child process.
Loading

0 comments on commit d9cb978

Please sign in to comment.