|
2 | 2 | This file contains integration tests. We use the CLI to exercise functionality of |
3 | 3 | various DFFML classes and constructs. |
4 | 4 | """ |
| 5 | +import re |
| 6 | +import os |
5 | 7 | import io |
| 8 | +import json |
6 | 9 | import inspect |
7 | 10 | import pathlib |
| 11 | +import asyncio |
8 | 12 | import contextlib |
| 13 | +import unittest.mock |
9 | 14 |
|
| 15 | +from dffml.df.types import Operation, DataFlow |
10 | 16 | from dffml.cli.cli import CLI |
| 17 | +from dffml.service.dev import Develop |
| 18 | +from dffml.util.packaging import is_develop |
| 19 | +from dffml.util.entrypoint import load |
11 | 20 | from dffml.util.asynctestcase import AsyncTestCase |
12 | 21 |
|
13 | 22 | from .test_cli import non_existant_tempfile |
14 | 23 |
|
15 | 24 |
|
| 25 | +def relative_path(*args): |
| 26 | + """ |
| 27 | + Returns a pathlib.Path object with the path relative to this file. |
| 28 | + """ |
| 29 | + target = pathlib.Path(__file__).parents[0] / args[0] |
| 30 | + for path in list(args)[1:]: |
| 31 | + target /= path |
| 32 | + return target |
| 33 | + |
| 34 | + |
| 35 | +@contextlib.contextmanager |
| 36 | +def relative_chdir(*args): |
| 37 | + """ |
| 38 | + Change directory to a location relative to the location of this file. |
| 39 | + """ |
| 40 | + target = relative_path(*args) |
| 41 | + orig_dir = os.getcwd() |
| 42 | + try: |
| 43 | + os.chdir(target) |
| 44 | + yield target |
| 45 | + finally: |
| 46 | + os.chdir(orig_dir) |
| 47 | + |
| 48 | + |
16 | 49 | class IntegrationCLITestCase(AsyncTestCase): |
| 50 | + REQUIRED_PLUGINS = [] |
| 51 | + |
17 | 52 | async def setUp(self): |
18 | 53 | super().setUp() |
| 54 | + if not all(map(is_develop, self.REQUIRED_PLUGINS)): |
| 55 | + self.skipTest( |
| 56 | + f"Required plugins: {', '.join(self.REQUIRED_PLUGINS)} must be installed in development mode" |
| 57 | + ) |
19 | 58 | self.stdout = io.StringIO() |
20 | 59 | self._stack = contextlib.ExitStack().__enter__() |
21 | 60 |
|
@@ -94,3 +133,119 @@ async def test_memory_to_csv(self): |
94 | 133 | ) |
95 | 134 | + "\n", |
96 | 135 | ) |
| 136 | + |
| 137 | + |
| 138 | +class TestDevelop(IntegrationCLITestCase): |
| 139 | + |
| 140 | + REQUIRED_PLUGINS = ["shouldi"] |
| 141 | + |
| 142 | + async def test_export(self): |
| 143 | + stdout = io.StringIO() |
| 144 | + # Use shouldi's dataflow for tests |
| 145 | + with relative_chdir("..", "examples", "shouldi"): |
| 146 | + with unittest.mock.patch("sys.stdout.buffer.write") as write: |
| 147 | + await Develop.cli("export", "shouldi.cli:DATAFLOW") |
| 148 | + DataFlow._fromdict(**json.loads(write.call_args[0][0])) |
| 149 | + |
| 150 | + |
| 151 | +class TestDataFlow(IntegrationCLITestCase): |
| 152 | + |
| 153 | + REQUIRED_PLUGINS = ["shouldi", "dffml-config-yaml", "dffml-feature-git"] |
| 154 | + |
| 155 | + async def setUp(self): |
| 156 | + await super().setUp() |
| 157 | + # Use shouldi's dataflow for tests |
| 158 | + self.DATAFLOW = list( |
| 159 | + load( |
| 160 | + "shouldi.cli:DATAFLOW", |
| 161 | + relative=relative_path("..", "examples", "shouldi"), |
| 162 | + ) |
| 163 | + )[0] |
| 164 | + |
| 165 | + async def test_diagram_default(self): |
| 166 | + filename = self.mktempfile() + ".json" |
| 167 | + pathlib.Path(filename).write_text(json.dumps(self.DATAFLOW.export())) |
| 168 | + with contextlib.redirect_stdout(self.stdout): |
| 169 | + await CLI.cli( |
| 170 | + "dataflow", "diagram", filename, |
| 171 | + ) |
| 172 | + stdout = self.stdout.getvalue() |
| 173 | + # Check that a subgraph is being made for each operation |
| 174 | + self.assertTrue(re.findall(r"subgraph.*run_bandit", stdout)) |
| 175 | + # Check that all stages are included |
| 176 | + for check in ["Processing", "Output", "Cleanup"]: |
| 177 | + self.assertIn(f"{check} Stage", stdout) |
| 178 | + |
| 179 | + async def test_diagram_simple(self): |
| 180 | + filename = self.mktempfile() + ".json" |
| 181 | + pathlib.Path(filename).write_text(json.dumps(self.DATAFLOW.export())) |
| 182 | + with contextlib.redirect_stdout(self.stdout): |
| 183 | + await CLI.cli( |
| 184 | + "dataflow", "diagram", "-simple", filename, |
| 185 | + ) |
| 186 | + # Check that a subgraph is not being made for each operation |
| 187 | + self.assertFalse( |
| 188 | + re.findall(r"subgraph.*run_bandit", self.stdout.getvalue()) |
| 189 | + ) |
| 190 | + |
| 191 | + async def test_diagram_single_stage(self): |
| 192 | + filename = self.mktempfile() + ".json" |
| 193 | + pathlib.Path(filename).write_text(json.dumps(self.DATAFLOW.export())) |
| 194 | + with contextlib.redirect_stdout(self.stdout): |
| 195 | + await CLI.cli( |
| 196 | + "dataflow", "diagram", "-stages", "processing", "--", filename, |
| 197 | + ) |
| 198 | + stdout = self.stdout.getvalue() |
| 199 | + # Check that the single stage is not its own subgraph |
| 200 | + for check in ["Processing", "Output", "Cleanup"]: |
| 201 | + self.assertNotIn(f"{check} Stage", stdout) |
| 202 | + |
| 203 | + async def test_diagram_multi_stage(self): |
| 204 | + filename = self.mktempfile() + ".json" |
| 205 | + pathlib.Path(filename).write_text(json.dumps(self.DATAFLOW.export())) |
| 206 | + with contextlib.redirect_stdout(self.stdout): |
| 207 | + await CLI.cli( |
| 208 | + "dataflow", |
| 209 | + "diagram", |
| 210 | + "-stages", |
| 211 | + "processing", |
| 212 | + "output", |
| 213 | + "--", |
| 214 | + filename, |
| 215 | + ) |
| 216 | + stdout = self.stdout.getvalue() |
| 217 | + # Check that the single stage is not its own subgraph |
| 218 | + for check in ["Processing", "Output"]: |
| 219 | + self.assertIn(f"{check} Stage", stdout) |
| 220 | + for check in ["Cleanup"]: |
| 221 | + self.assertNotIn(f"{check} Stage", stdout) |
| 222 | + |
| 223 | + async def test_merge(self): |
| 224 | + # Write out shouldi dataflow |
| 225 | + orig = self.mktempfile() + ".json" |
| 226 | + pathlib.Path(orig).write_text(json.dumps(self.DATAFLOW.export())) |
| 227 | + # Import from feature/git |
| 228 | + transform_to_repo = Operation.load("dffml.mapping.create") |
| 229 | + lines_of_code_by_language, lines_of_code_to_comments = list( |
| 230 | + load( |
| 231 | + "dffml_feature_git.feature.operations:lines_of_code_by_language", |
| 232 | + "dffml_feature_git.feature.operations:lines_of_code_to_comments", |
| 233 | + relative=relative_path("..", "feature", "git"), |
| 234 | + ) |
| 235 | + ) |
| 236 | + # Create new dataflow |
| 237 | + override = DataFlow.auto( |
| 238 | + transform_to_repo, |
| 239 | + lines_of_code_by_language, |
| 240 | + lines_of_code_to_comments, |
| 241 | + ) |
| 242 | + # TODO Modify and compare against yaml in docs example |
| 243 | + # Write out override dataflow |
| 244 | + created = self.mktempfile() + ".json" |
| 245 | + pathlib.Path(created).write_text(json.dumps(override.export())) |
| 246 | + # Merge the two |
| 247 | + with contextlib.redirect_stdout(self.stdout): |
| 248 | + await CLI.cli( |
| 249 | + "dataflow", "merge", orig, created, |
| 250 | + ) |
| 251 | + DataFlow._fromdict(**json.loads(self.stdout.getvalue())) |
0 commit comments