# pytest-expectdir
![tests](https://github.com/hl037/pytest-expectdir/actions/workflows/tests.yml/badge.svg)
[![codecov](https://codecov.io/gh/hl037/pytest-expectdir/branch/master/graph/badge.svg?token=IEML9TAP59)](https://codecov.io/gh/hl037/pytest-expectdir)
This pytest plugin provides an easy way to test file generation and file-system transformation.
## Install
```
pip install pytest-expectdir
```
## Usage
Here is the workflow :
Create a directory containing files and directories expected to be generated, and optionally one with your initial data so that it looks like this :
```
my_pkg/
├ my_pkg/
│ └ ...
└ tests/
├ test_feature.py
└ test_feature/
├ initial (optional)/
│ └ ... your initial data
└ expected/
└ ... expected output tree
```
Then you write your test as follow :
`test_feature.py`
```
def test_feature(expectdir):
with expectdir('test_feature') as output_dir:
# Do whatever you want inside output_dir, which is a temporary directory copied from initial/
# At the end of the with, output_dir gets compared with expected
# ...And you get a fancy report of the difference if there are (as an AssertionError).
```
Note that you can also pass manually the keyword arguments `initial` and `expected` to `expectdir`. If, for example, you have multiple tests ending up with the same expected result, or with the same initial one.
The following is equivalent to the previous example :
```
def test_feature(expectdir):
with expectdir(initial='data_test_feature/initial', expected='data_test_feature/expected') as output_dir:
# ...
```
If your test data follows this schema :
```
tests/
├ test_feature.py
└ test_feature/
└ TestCaseClassName (if one)/
└ test_method
├ initial (optional)/
│ └ ...
└ expected
└ ...
```
(like the first example), then you can even omit the parameters :
```
def test_feature(expectdir):
with expectdir() as output_dir:
# ...
```
## API
### (`pytest.fixture`) `expectdir(datapath=None, *, initial=None, expected=None, current_dir_replace_string=None) -> contextmanager as outputDir:Path`
The main fixture. Its value is a function that returns a context manager. The context manager will return (when opened) a path to a temporary directory that will get compared to the Expected directory at closing. An AssertionError will then be raised if the two directory are not the same. `.gitkeep` files, conventionally used to keep empty directories are ignored.
You also may require to the content of some file containing the path where the test is executed. Just before executing what is in the `with`, the string passed to `current_dir_replace_string` is replaced by temporary directory path in all files in `initial/`. Also, after the with block, and before checking the temporary directory is equal to the expected one, all occurences of the temporary directory path is replaced by `current_dir_replace_string`. If `None` is passed, no replacement is done.
The function chooses an optional initial directory and a required expected directory as follow :
#### Expected
* If the `expected` keyword argument is provided, it's this directory that will be used.
* Else, if the `datapath` positional argument is provided, expected will be `datapath/"expected"`.
* Else, the test path will be used as fallback, i.e. `currentModuleDirectory/TestCaseClassName/test_method/expected` if inside a testCase class, else, `currentModuleDirectory/test_function/expected` if the test is a standalone function.
* If the selected path does not exist, raises a FileNotFoundError.
#### Initial
* If the `initial` keyword argument is provided and equal to `__empty__`, then the initial directory will be empty.
* If the `initial` keyword argument is provided and is a different string or a `Path` instance, it's this directory that will be used.
* Else, if the `datapath` positional argument is provided, expected will be `datapath/"initial"`.
* Else, if the `expected` keyword argument is **not** provided, the test path will be used as fallback, i.e. `currentModuleDirectory/TestCaseClassName/test_method/initial` if inside a TestCase class, else, `currentModuleDirectory/test_function/initial` if the test is a standalone function.
* Else, the initial directory will be empty.
* If the initial keyword argument is a Path, and this path does not exists, raises a FileNotFoundError.
### (`pytest.fixture`) `expectdir(datapath=None, *, initial=None, expected=None, current_dir_replace_string="{{current_directory}}") -> contextmanager as outputDir:Path`
Equivalent to expectDir, but with `"{{current_directory}}"` as default value for `current_dir_replace_string`.
### `cmpdir(candidate:Path, expected:Path) -> Tuple[result:bool, Tuple[candidate_only:list[Path], expected_only:list[Path], different:list[Path]]]`
Compare two directories recursively, and list files only in the first, only on the second, and in both but different.
The result is `True` if the directories are identical.
When a subdirectory is present only in one of the compared directories, only the subdirectory itself is listed (not all its content).
Files `.gitkeep` are ignored.
### `formatDiff(file_output:TextIO, candidate:Path, expected:Path, diffRes:Tuple[candidate_only:list[Path], expected_only:list[Path], different:list[Path]]) -> None`
Takes the result of `cmpdir`, and print to `file_output` the diff summary.
### `formatFileDiff(file_output:TextIO, lines_candidate:Iterable[str], lines_expected:Iterable[str], context=3, indent=' ') -> None`
Format the diff of two files, and output to `file_output`. `context` is the number of identical to show before and after insertion / deletion for context. `indent` is the line prefix, so that the output is indented.
## How Fancy ?
Here is a sample from the tests :
```
In [1]: import sys
...: from pytest_expectdir.plugin import cmpdir, formatDiff
...: initial = './tests/data/test3/initial/'
...: expected = './tests/data/test3/expected/'
...: formatDiff(sys.stdout, initial, expected, cmpdir(initial, expected)[1])
Directory ./tests/data/test3/expected/ (expected) is different from ./tests/data/test3/initial/ (candidate).
Missing in candidate :
dir3/
f1
Extra in candidate :
dir2/
f4
In both directories but different content:
f3:
- This line is removed
- And this one too
This is a complex test
- Hello 3
+ Hello 3 And replaced ones
With some lines
+ And added lines
And otherlines
common 1
common 2
[...] --- expected:11 / candidate:10 ---
common 6
common 7
common 8
- and diff 1
+ diff
dir4/f3:
- This line is removed
- And this one too
This is a complex test
- Hello 3
+ Hello 3 And replaced ones
With some lines
+ And added lines
And otherlines
common 1
common 2
[...] --- expected:11 / candidate:10 ---
common 6
common 7
common 8
- and diff 1
+ diff
```
![Preview Image](https://github.com/hl037/pytest-expectdir/blob/master/screenshot.png?raw=true)
# 1.2.0
Added Replacement string for the current directory
# 1.1.4
Fix typo
# 1.1.3
Fix Coverage.io badge in README
# 1.1.2
Added unit test for full coverage
# 1.1.1
Fix Exception not forwarded
# 1.1.0
Fallback for expectdir to `module_stem/class(if one)/function` if datadir is None
# 1.0.0
`expectdir -> (datapath=None, *, initial=None, expected=None) -> contextmanager -> tmpdir:Path` fixture
Raw data
{
"_id": null,
"home_page": "https://github.com/hl037/pytest-expectdir",
"name": "pytest-expectdir",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": "",
"keywords": "pytest test unittest directory file",
"author": "L\u00e9o Falventin Hauchecorne",
"author_email": "hl037.prog@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/c4/4c/dcaf2bf29036e3feb616edd32056be46332f46378d4d72d282af606abdfa/pytest-expectdir-1.2.0.tar.gz",
"platform": null,
"description": "# pytest-expectdir\n\n![tests](https://github.com/hl037/pytest-expectdir/actions/workflows/tests.yml/badge.svg)\n[![codecov](https://codecov.io/gh/hl037/pytest-expectdir/branch/master/graph/badge.svg?token=IEML9TAP59)](https://codecov.io/gh/hl037/pytest-expectdir)\n\nThis pytest plugin provides an easy way to test file generation and file-system transformation.\n\n## Install\n\n```\npip install pytest-expectdir\n```\n\n## Usage\n\nHere is the workflow :\n\nCreate a directory containing files and directories expected to be generated, and optionally one with your initial data so that it looks like this :\n\n```\nmy_pkg/\n\u251c my_pkg/\n\u2502 \u2514 ...\n\u2514 tests/\n \u251c test_feature.py\n \u2514 test_feature/\n \u251c initial (optional)/\n \u2502 \u2514 ... your initial data\n \u2514 expected/\n \u2514 ... expected output tree\n\n```\n\nThen you write your test as follow :\n\n`test_feature.py`\n```\ndef test_feature(expectdir):\n with expectdir('test_feature') as output_dir:\n # Do whatever you want inside output_dir, which is a temporary directory copied from initial/\n # At the end of the with, output_dir gets compared with expected\n # ...And you get a fancy report of the difference if there are (as an AssertionError).\n```\n\nNote that you can also pass manually the keyword arguments `initial` and `expected` to `expectdir`. If, for example, you have multiple tests ending up with the same expected result, or with the same initial one.\n\nThe following is equivalent to the previous example : \n\n```\ndef test_feature(expectdir):\n with expectdir(initial='data_test_feature/initial', expected='data_test_feature/expected') as output_dir:\n # ...\n```\n\n\nIf your test data follows this schema :\n\n```\ntests/\n\u251c test_feature.py\n\u2514 test_feature/\n \u2514 TestCaseClassName (if one)/\n \u2514 test_method\n \u251c initial (optional)/\n \u2502 \u2514 ...\n \u2514 expected\n \u2514 ...\n```\n\n(like the first example), then you can even omit the parameters :\n\n```\ndef test_feature(expectdir):\n with expectdir() as output_dir:\n # ...\n```\n\n## API\n\n### (`pytest.fixture`) `expectdir(datapath=None, *, initial=None, expected=None, current_dir_replace_string=None) -> contextmanager as outputDir:Path`\n\nThe main fixture. Its value is a function that returns a context manager. The context manager will return (when opened) a path to a temporary directory that will get compared to the Expected directory at closing. An AssertionError will then be raised if the two directory are not the same. `.gitkeep` files, conventionally used to keep empty directories are ignored.\n\nYou also may require to the content of some file containing the path where the test is executed. Just before executing what is in the `with`, the string passed to `current_dir_replace_string` is replaced by temporary directory path in all files in `initial/`. Also, after the with block, and before checking the temporary directory is equal to the expected one, all occurences of the temporary directory path is replaced by `current_dir_replace_string`. If `None` is passed, no replacement is done.\n\nThe function chooses an optional initial directory and a required expected directory as follow :\n\n#### Expected\n* If the `expected` keyword argument is provided, it's this directory that will be used.\n* Else, if the `datapath` positional argument is provided, expected will be `datapath/\"expected\"`.\n* Else, the test path will be used as fallback, i.e. `currentModuleDirectory/TestCaseClassName/test_method/expected` if inside a testCase class, else, `currentModuleDirectory/test_function/expected` if the test is a standalone function.\n* If the selected path does not exist, raises a FileNotFoundError.\n\n#### Initial\n* If the `initial` keyword argument is provided and equal to `__empty__`, then the initial directory will be empty.\n* If the `initial` keyword argument is provided and is a different string or a `Path` instance, it's this directory that will be used.\n* Else, if the `datapath` positional argument is provided, expected will be `datapath/\"initial\"`.\n* Else, if the `expected` keyword argument is **not** provided, the test path will be used as fallback, i.e. `currentModuleDirectory/TestCaseClassName/test_method/initial` if inside a TestCase class, else, `currentModuleDirectory/test_function/initial` if the test is a standalone function.\n* Else, the initial directory will be empty.\n* If the initial keyword argument is a Path, and this path does not exists, raises a FileNotFoundError.\n\n### (`pytest.fixture`) `expectdir(datapath=None, *, initial=None, expected=None, current_dir_replace_string=\"{{current_directory}}\") -> contextmanager as outputDir:Path`\n\nEquivalent to expectDir, but with `\"{{current_directory}}\"` as default value for `current_dir_replace_string`.\n\n### `cmpdir(candidate:Path, expected:Path) -> Tuple[result:bool, Tuple[candidate_only:list[Path], expected_only:list[Path], different:list[Path]]]`\n\nCompare two directories recursively, and list files only in the first, only on the second, and in both but different.\n\nThe result is `True` if the directories are identical.\n\nWhen a subdirectory is present only in one of the compared directories, only the subdirectory itself is listed (not all its content).\n\nFiles `.gitkeep` are ignored.\n\n### `formatDiff(file_output:TextIO, candidate:Path, expected:Path, diffRes:Tuple[candidate_only:list[Path], expected_only:list[Path], different:list[Path]]) -> None`\n\nTakes the result of `cmpdir`, and print to `file_output` the diff summary.\n\n### `formatFileDiff(file_output:TextIO, lines_candidate:Iterable[str], lines_expected:Iterable[str], context=3, indent=' ') -> None`\n\nFormat the diff of two files, and output to `file_output`. `context` is the number of identical to show before and after insertion /\u00a0deletion for context. `indent` is the line prefix, so that the output is indented.\n\n\n## How Fancy ?\n\nHere is a sample from the tests : \n\n```\nIn [1]: import sys\n ...: from pytest_expectdir.plugin import cmpdir, formatDiff\n ...: initial = './tests/data/test3/initial/'\n ...: expected = './tests/data/test3/expected/'\n ...: formatDiff(sys.stdout, initial, expected, cmpdir(initial, expected)[1])\nDirectory ./tests/data/test3/expected/ (expected) is different from ./tests/data/test3/initial/ (candidate).\nMissing in candidate :\ndir3/\nf1\nExtra in candidate :\ndir2/\nf4\nIn both directories but different content:\n\nf3:\n - This line is removed\n - And this one too\n This is a complex test\n - Hello 3\n + Hello 3 And replaced ones\n With some lines\n + And added lines\n And otherlines\n common 1\n common 2\n [...] --- expected:11 / candidate:10 ---\n common 6\n common 7\n common 8\n - and diff 1\n + diff\n\ndir4/f3:\n - This line is removed\n - And this one too\n This is a complex test\n - Hello 3\n + Hello 3 And replaced ones\n With some lines\n + And added lines\n And otherlines\n common 1\n common 2\n [...] --- expected:11 / candidate:10 ---\n common 6\n common 7\n common 8\n - and diff 1\n + diff\n```\n\n![Preview Image](https://github.com/hl037/pytest-expectdir/blob/master/screenshot.png?raw=true)\n\n\n\n# 1.2.0\n Added Replacement string for the current directory\n# 1.1.4\n Fix typo\n# 1.1.3\n Fix Coverage.io badge in README\n# 1.1.2\n Added unit test for full coverage\n# 1.1.1\n Fix Exception not forwarded\n# 1.1.0\n Fallback for expectdir to `module_stem/class(if one)/function` if datadir is None\n# 1.0.0\n `expectdir -> (datapath=None, *, initial=None, expected=None) -> contextmanager -> tmpdir:Path` fixture\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A pytest plugin to provide initial/expected directories, and check a test transforms the initial directory to the expected one",
"version": "1.2.0",
"split_keywords": [
"pytest",
"test",
"unittest",
"directory",
"file"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "2c84fc3c513a95705e39992b007a9cf641af963249c1b4c9cf8857dca9a6e6f6",
"md5": "45f836d4c88fab0aae9dcb3b21e0de01",
"sha256": "8fa19e7c0b69d6bfd0e81c6e33c9a8283eb14a7bab8408067e430a827a03f993"
},
"downloads": -1,
"filename": "pytest_expectdir-1.2.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "45f836d4c88fab0aae9dcb3b21e0de01",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 8159,
"upload_time": "2023-03-19T11:49:10",
"upload_time_iso_8601": "2023-03-19T11:49:10.965061Z",
"url": "https://files.pythonhosted.org/packages/2c/84/fc3c513a95705e39992b007a9cf641af963249c1b4c9cf8857dca9a6e6f6/pytest_expectdir-1.2.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "c44cdcaf2bf29036e3feb616edd32056be46332f46378d4d72d282af606abdfa",
"md5": "a1a12b2c3df21b527d6e1ec293a3e571",
"sha256": "145b7f86d4e8ae7c7cace3007f32d68afbdd4d18c392b3d5792794f8e9d51fae"
},
"downloads": -1,
"filename": "pytest-expectdir-1.2.0.tar.gz",
"has_sig": false,
"md5_digest": "a1a12b2c3df21b527d6e1ec293a3e571",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 9804,
"upload_time": "2023-03-19T11:49:12",
"upload_time_iso_8601": "2023-03-19T11:49:12.976754Z",
"url": "https://files.pythonhosted.org/packages/c4/4c/dcaf2bf29036e3feb616edd32056be46332f46378d4d72d282af606abdfa/pytest-expectdir-1.2.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-03-19 11:49:12",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "hl037",
"github_project": "pytest-expectdir",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "pytest-expectdir"
}