Skip to content

Commit 8b5e276

Browse files
committed
mfc.sh test: --add-new-variables option
1 parent 9ab5e21 commit 8b5e276

File tree

4 files changed

+55
-30
lines changed

4 files changed

+55
-30
lines changed

docs/documentation/testing.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ To restrict to a given range, use the `--from` (`-f`) and `--to` (`-t`) options.
1717

1818
### Creating Tests
1919

20-
To (re)generate *golden files*, append the `-g` (i.e `--generate`) option:
20+
To (re)generate *golden files*, append the `--generate` option:
2121
```console
22-
$ ./mfc.sh test -g -j 8
22+
$ ./mfc.sh test --generate -j 8
2323
```
2424

2525
It is recommended that a range be specified when generating golden files for new test cases, as described in the previous section, in an effort not to regenerate the golden files of existing test cases.
2626

27+
**Note:** If you output new variables and want to update the golden files to include these without modifying the original data, use the `--add-new-variables` option instead.
28+
2729
Adding a new test case can be done by modifying [cases.py](https://github.com/MFlowCode/MFC/tree/master/toolchain/mfc/test/cases.py). The function `generate_cases` is responsible for generating the list of test cases. Loops and conditionals are used to vary parameters, whose defaults can be found in the `BASE_CFG` case object within [case.py](https://github.com/MFlowCode/MFC/tree/master/toolchain/mfc/test/case.py). The function operates on two variables:
2830

2931
- `stack`: A stack that holds the variations to the default case parameters. By pushing and popping the stack inside loops and conditionals, it is easier to nest test case descriptions, as it holds the variations that are common to all future test cases within the same indentation level (in most scenarios).

toolchain/mfc/args.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def parse(config):
2020
)
2121

2222
parsers = parser.add_subparsers(dest="command")
23-
23+
2424
run = parsers.add_parser(name="run", help="Run a case with MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
2525
test = parsers.add_parser(name="test", help="Run MFC's test suite.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
2626
build = parsers.add_parser(name="build", help="Build MFC and its dependencies.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@@ -63,15 +63,19 @@ def add_common_arguments(p, mask = None):
6363

6464
# === TEST ===
6565
add_common_arguments(test, "t")
66-
test.add_argument( "--generate", action="store_true", help="Generate golden files.")
6766
test.add_argument("-l", "--list", action="store_true", help="List all available tests.")
6867
test.add_argument("-f", "--from", default=TEST_CASES[0].get_uuid(), type=str, help="First test UUID to run.")
6968
test.add_argument("-t", "--to", default=TEST_CASES[-1].get_uuid(), type=str, help="Last test UUID to run.")
7069
test.add_argument("-o", "--only", nargs="+", type=str, default=[], metavar="L", help="Only run tests with UUIDs or hashes L.")
7170
test.add_argument("-b", "--binary", choices=binaries, type=str, default=None, help="(Serial) Override MPI execution binary")
7271
test.add_argument("-r", "--relentless", action="store_true", default=False, help="Run all tests, even if multiple fail.")
7372
test.add_argument("-a", "--test-all", action="store_true", default=False, help="Run the Post Process Tests too.")
73+
7474
test.add_argument("--case-optimization", action="store_true", default=False, help="(GPU Optimization) Compile MFC targets with some case parameters hard-coded.")
75+
76+
test_meg = test.add_mutually_exclusive_group()
77+
test_meg.add_argument("--generate", action="store_true", default=False, help="(Test Generation) Generate golden files.")
78+
test_meg.add_argument("--add-new-variables", action="store_true", default=False, help="(Test Generation) If new variables are found in D/ when running tests, add them to the golden files.")
7579

7680
# === RUN ===
7781
engines = [ e.slug for e in ENGINES ]

toolchain/mfc/test/pack.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os, re, math, dataclasses
1+
import os, re, math, typing, dataclasses
22

33
from pathlib import Path
44

@@ -60,25 +60,41 @@ def __repr__(self) -> str:
6060
return self.get().__repr__()
6161

6262

63+
# This class maps to the data contained in one file in D/
6364
@dataclasses.dataclass(repr=False)
6465
class PackEntry:
6566
filepath: str
66-
doubles: list
67+
doubles: typing.List[float]
6768

6869
def __repr__(self) -> str:
6970
return f"{self.filepath} {' '.join([ str(d) for d in self.doubles ])}"
7071

7172

72-
@dataclasses.dataclass
73+
# This class maps to the data contained in the entirety of D/: it is tush a list
74+
# of PackEntry classes.
7375
class Pack:
74-
entries: list
76+
entries: typing.Dict[str, PackEntry]
77+
78+
def __init__(self):
79+
self.entries = {}
80+
81+
def __init__(self, entries: typing.List[PackEntry]):
82+
self.entries = {}
83+
for entry in entries:
84+
self.set(entry)
85+
86+
def find(self, filepath: str) -> PackEntry:
87+
return self.entries.get(filepath, None)
88+
89+
def set(self, entry: PackEntry):
90+
self.entries[entry.filepath] = entry
7591

7692
def save(self, filepath: str):
77-
common.file_write(filepath, '\n'.join([ str(e) for e in self.entries ]))
93+
common.file_write(filepath, '\n'.join([ str(e) for e in sorted(self.entries.values(), key=lambda x: x.filepath) ]))
7894

7995

8096
def load(filepath: str) -> Pack:
81-
entries: list = []
97+
entries: typing.List[PackEntry] = []
8298

8399
for line in common.file_read(filepath).splitlines():
84100
if common.isspace(line):
@@ -103,18 +119,14 @@ def generate(case: case.TestCase) -> Pack:
103119
for filepath in list(Path(D_dir).rglob("*.dat")):
104120
short_filepath = str(filepath).replace(f'{case_dir}', '')[1:].replace("\\", "/")
105121

106-
data_content = common.file_read(filepath)
107-
108-
# 2 or more (contiguous) spaces
109-
pattern = r"([ ]{2,})"
110-
111-
numbers_str = re.sub(pattern, " ", data_content.replace('\n', '')).strip()
112-
113-
doubles: list = [ float(e) for e in numbers_str.split(' ') ]
122+
try:
123+
doubles = [ float(e) for e in re.sub(r"[\n\t\s]+", " ", common.file_read(filepath)).strip().split(' ') ]
124+
except ValueError:
125+
raise MFCException(f"Test {case}: Failed to interpret the content of [magenta]{filepath}[/magenta] as a list of floating point numbers.")
114126

115127
for double in doubles:
116128
if math.isnan(double):
117-
raise MFCException(f"Test {case}: A NaN was found while generating a pack file.")
129+
raise MFCException(f"Test {case}: A NaN was found in {filepath} while generating a pack file.")
118130

119131
entries.append(PackEntry(short_filepath,doubles))
120132

@@ -144,21 +156,19 @@ def check_tolerance(case: case.TestCase, candidate: Pack, golden: Pack, tol: flo
144156

145157
# Compare entry-count
146158
if len(candidate.entries) != len(golden.entries):
147-
raise MFCException(f"Test {case}: Line count didn't match.")
159+
raise MFCException(f"Test {case}: Line count does not match.")
148160

149161
# For every entry in the golden's pack
150-
for gIndex, gEntry in enumerate(golden.entries):
162+
for gFilepath, gEntry in golden.entries.items():
151163
# Find the corresponding entry in the candidate's pack
152-
cIndex, cEntry = common.find(lambda i, e: e.filepath == gEntry.filepath, candidate.entries)
153-
154-
if cIndex == None:
155-
raise MFCException(f"Test {case}: No reference of {gEntry.filepath} in the candidate's pack.")
164+
cEntry = candidate.find(gFilepath)
156165

157-
filepath: str = gEntry.filepath
166+
if cEntry == None:
167+
raise MFCException(f"Test {case}: No reference of {gFilepath} in the candidate's pack.")
158168

159169
# Compare variable-count
160170
if len(gEntry.doubles) != len(cEntry.doubles):
161-
raise MFCException(f"Test {case}: Variable count didn't match for {filepath}.")
171+
raise MFCException(f"Test {case}: Variable count didn't match for {gFilepath}.")
162172

163173
# Check if each variable is within tolerance
164174
for valIndex, (gVal, cVal) in enumerate(zip(gEntry.doubles, cEntry.doubles)):
@@ -168,7 +178,7 @@ def check_tolerance(case: case.TestCase, candidate: Pack, golden: Pack, tol: flo
168178

169179
def raise_err(msg: str):
170180
raise MFCException(f"""\
171-
Test {case}: Variable n°{valIndex+1} (1-indexed) in {filepath} {msg}:
181+
Test {case}: Variable n°{valIndex+1} (1-indexed) in {gFilepath} {msg}:
172182
- Description: {case.trace}
173183
- Candidate: {cVal}
174184
- Golden: {gVal}

toolchain/mfc/test/test.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,18 @@ def handle_case(test: TestCase):
153153
pack.save(golden_filepath)
154154
else:
155155
if not os.path.isfile(golden_filepath):
156-
raise MFCException(f"Test {test}: Golden file doesn't exist! To generate golden files, use the '-g' flag.")
156+
raise MFCException(f"Test {test}: The golden file does not exist! To generate golden files, use the '--generate' flag.")
157157

158-
packer.check_tolerance(test, pack, packer.load(golden_filepath), tol)
158+
golden = packer.load(golden_filepath)
159+
160+
if ARG("add_new_variables"):
161+
for pfilepath, pentry in pack.entries.items():
162+
if golden.find(pfilepath) is None:
163+
golden.set(pentry)
164+
165+
golden.save(golden_filepath)
166+
else:
167+
packer.check_tolerance(test, pack, packer.load(golden_filepath), tol)
159168

160169
if ARG("test_all"):
161170
test.params.update({

0 commit comments

Comments
 (0)