blob: 4a006d6d9081500818cbca7ae1111ff18a7ac366 [file] [log] [blame]
Jeremy Johnson6179c212022-01-13 13:46:35 +00001#!/usr/bin/env python3
evacha01ad8e1e22024-03-19 12:42:17 +00002# Copyright (c) 2021-2024, ARM Limited.
Jeremy Johnson6179c212022-01-13 13:46:35 +00003# SPDX-License-Identifier: Apache-2.0
4"""This script converts generated tests into conformance tests.
5
6It can convert a framework unit test or a reference model unit test.
7It expects the tests have been already run on the reference model
8so it can capture the result as the expected result.
9"""
10import argparse
11import json
12import logging
13import os
Jeremy Johnson1271c442023-09-05 11:39:26 +010014import shutil
Jeremy Johnson6179c212022-01-13 13:46:35 +000015from pathlib import Path
16from typing import Optional
17
18from json2fbbin.json2fbbin import fbbin_to_json
19from json2numpy.json2numpy import npy_to_json
Jeremy Johnson1271c442023-09-05 11:39:26 +010020from schemavalidation.schemavalidation import TestDescSchemaValidator
Jeremy Johnson6179c212022-01-13 13:46:35 +000021
22logging.basicConfig(level=logging.INFO)
23logger = logging.getLogger("convert2conformance")
24
Jeremy Johnson6179c212022-01-13 13:46:35 +000025
26NAME_FLATBUFFER_DIR = ["flatbuffer-", "_FW_"]
27NAME_DESC_FILENAME = "desc.json"
28NAME_CONFORMANCE_RESULT_PREFIX = "Conformance-"
29NAME_REFMODEL_RUN_RESULT_SUFFIX = ".runner.tosa_refmodel_sut_run.npy"
30
Jeremy Johnson88588622022-07-12 16:42:29 +010031PROFILES_LIST = ["tosa-bi", "tosa-mi"]
32
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +010033OUTPUT_TYPE_JSON = "json"
34OUTPUT_TYPE_BINARY = "binary"
35OUTPUT_TYPE_BOTH = "both"
36OUTPUT_TYPES = (OUTPUT_TYPE_JSON, OUTPUT_TYPE_BINARY, OUTPUT_TYPE_BOTH)
37OUTPUT_TYPE_DEFAULT = OUTPUT_TYPE_JSON
38
Jeremy Johnson6179c212022-01-13 13:46:35 +000039
40def parse_args(argv):
41 """Parse the arguments."""
Jeremy Johnson1271c442023-09-05 11:39:26 +010042 # Set prog for when we are called via tosa_verif_conformance_generator
43 parser = argparse.ArgumentParser(prog="convert2conformance")
Jeremy Johnson6179c212022-01-13 13:46:35 +000044 parser.add_argument(
45 "test_dir",
46 default=Path.cwd(),
47 type=Path,
48 nargs="?",
49 help="The test directory to convert (default is CWD)",
50 )
51 parser.add_argument(
Jeremy Johnsonf0348ea2023-09-27 16:10:59 +010052 "--schema-path",
53 "--operator-fbs",
54 dest="schema_path",
Jeremy Johnson6179c212022-01-13 13:46:35 +000055 type=Path,
56 required=True,
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +010057 help="Path to reference model schema.",
Jeremy Johnsonf0348ea2023-09-27 16:10:59 +010058 )
59 parser.add_argument(
60 "--flatc-path",
61 dest="flatc_path",
62 type=Path,
63 required=True,
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +010064 help="Path to flatc executable.",
Jeremy Johnson6179c212022-01-13 13:46:35 +000065 )
66 parser.add_argument(
67 "--output-directory",
68 dest="output_dir",
69 type=Path,
70 default=Path.cwd() / "conformance",
71 help="Output directory (default is conformance in CWD)",
72 )
73 parser.add_argument(
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +010074 "--output-type",
75 dest="output_type",
76 choices=OUTPUT_TYPES,
77 default=OUTPUT_TYPE_DEFAULT,
78 help=f"Output file type produced (default is {OUTPUT_TYPE_DEFAULT})",
79 )
80 parser.add_argument(
Jeremy Johnson6179c212022-01-13 13:46:35 +000081 "--framework",
82 dest="framework",
83 choices=["tflite"],
84 default="tflite",
85 help="Framework to convert (default tflite)",
86 )
87 parser.add_argument(
88 "--framework-schema",
89 dest="framework_schema",
90 type=Path,
91 help="Framework schema needed to convert framework models",
92 )
93 parser.add_argument(
Jeremy Johnson88588622022-07-12 16:42:29 +010094 "--profile",
95 dest="profile",
96 choices=PROFILES_LIST,
97 action="append",
98 required=True,
99 help="Profiles this test is suitable for. May be repeated",
100 )
101 parser.add_argument(
Jeremy Johnsonfd05bb32023-02-07 16:39:24 +0000102 "--tag",
Jeremy Johnson76c6a552023-09-11 09:30:02 +0100103 dest="tags",
Jeremy Johnsonfd05bb32023-02-07 16:39:24 +0000104 action="append",
105 type=str,
Jeremy Johnson76c6a552023-09-11 09:30:02 +0100106 help="Optional string tag to mark this test with. May be repeated",
Jeremy Johnsonfd05bb32023-02-07 16:39:24 +0000107 )
108 parser.add_argument(
Jeremy Johnsondd8d9c22022-12-12 14:18:10 +0000109 "--strict",
110 dest="strict",
111 action="store_true",
112 help="Output directory must not contain the same test directory",
113 )
114 parser.add_argument(
Jeremy Johnson1271c442023-09-05 11:39:26 +0100115 "--lazy-data-generation",
116 action="store_true",
117 help="Enable lazy data generation (only for tosa-mi)",
118 )
119 parser.add_argument(
Jeremy Johnson6179c212022-01-13 13:46:35 +0000120 "-v", "--verbose", dest="verbose", action="store_true", help="Verbose operation"
121 )
122 args = parser.parse_args(argv)
Jeremy Johnson88588622022-07-12 16:42:29 +0100123
Jeremy Johnson6179c212022-01-13 13:46:35 +0000124 return args
125
126
Jeremy Johnson6179c212022-01-13 13:46:35 +0000127def find_framework_artifacts(framework: str, schema_path: Path, desc_file: Path):
128 """Check that any required schema has been supplied for conversion."""
129 if framework == "tflite":
130 if not schema_path:
131 raise Exception("the following arguments are required: --framework-schema")
132 elif not schema_path.is_file():
133 raise Exception(f"framework schema not found at {schema_path}")
134 model = desc_file.parent.parent / "model.tflite"
135 if not model.is_file():
136 raise Exception(f"Model file not found at {model}")
137 return schema_path, model
138 return None, None
139
140
141def get_framework_name(name_array: list, framework: str):
142 """Get the framework conversion directory name."""
143 name = ""
144 for part in name_array:
145 if part == "_FW_":
146 part = framework
147 name = f"{name}{part}"
148 return name
149
150
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100151def convert_flatbuffer_file(
152 output_type: str, flatc: Path, schema: Path, model_file: Path, output: Path
153):
154 """Convert and/or copy the flatbuffer binary."""
155 if output_type in (OUTPUT_TYPE_JSON, OUTPUT_TYPE_BOTH):
156 try:
157 fbbin_to_json(flatc, schema, model_file, output)
158 except Exception as e:
159 logger.error(f"Failed to convert flatbuffer binary:\n{e}")
160 return None
Jeremy Johnson6179c212022-01-13 13:46:35 +0000161
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100162 if model_file.name == "model.tflite":
163 file_name = "model-tflite.json"
164 os.rename(output / "model.json", output / file_name)
165 else:
166 file_name = model_file.stem + ".json"
167 if output_type in (OUTPUT_TYPE_BINARY, OUTPUT_TYPE_BOTH):
168 try:
169 shutil.copy(model_file, output)
170 except Exception as e:
171 logger.error(f"Failed to copy flatbuffer binary:\n{e}")
172 return None
173 # By default return the binary name (if we have created both)
174 file_name = model_file.name
175
Jeremy Johnson6179c212022-01-13 13:46:35 +0000176 return output / file_name
177
178
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100179def convert_numpy_file(
180 output_type: str, npy_file: Path, output: Path, outstem: Optional[str] = None
181):
182 """Convert and/or copy the numpy file."""
183 if output_type in (OUTPUT_TYPE_JSON, OUTPUT_TYPE_BOTH):
184 new_file = output / ((outstem if outstem else npy_file.stem) + ".json")
185 npy_to_json(npy_file, new_file)
186 if output_type in (OUTPUT_TYPE_BINARY, OUTPUT_TYPE_BOTH):
187 new_file = output / ((outstem + ".npy") if outstem else npy_file.name)
188 shutil.copy(npy_file, new_file)
Jeremy Johnson6179c212022-01-13 13:46:35 +0000189
190
191def update_desc_json(
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100192 output_type: str,
Jeremy Johnson88588622022-07-12 16:42:29 +0100193 test_dir: Path,
194 test_desc,
195 output_dir: Optional[Path] = None,
Jeremy Johnsone2b5e872023-09-14 17:02:09 +0100196 record_result=True,
Jeremy Johnson88588622022-07-12 16:42:29 +0100197 profiles=None,
Jeremy Johnsonfd05bb32023-02-07 16:39:24 +0000198 tags=None,
Jeremy Johnson6179c212022-01-13 13:46:35 +0000199):
Jeremy Johnsone2b5e872023-09-14 17:02:09 +0100200 """Update the desc.json format for conformance and optionally record result."""
Jeremy Johnson6179c212022-01-13 13:46:35 +0000201 ofm_files = []
202 cfm_files = []
203 if not output_dir:
204 output_dir = test_dir
205 for index, ofm in enumerate(test_desc["ofm_file"]):
206 ofm_path = test_dir / ofm
207 if not test_desc["expected_failure"]:
208 cfm = NAME_CONFORMANCE_RESULT_PREFIX + test_desc["ofm_name"][index]
Jeremy Johnsone2b5e872023-09-14 17:02:09 +0100209 if record_result:
Jeremy Johnson6179c212022-01-13 13:46:35 +0000210 if ofm_path.is_file():
211 # Use the desc.json name
212 ofm_refmodel = ofm_path
213 else:
214 # Adjust for renaming due to tosa_verif_run_tests
215 ofm_refmodel = ofm_path.with_suffix(NAME_REFMODEL_RUN_RESULT_SUFFIX)
216 # Create conformance result
217 if ofm_refmodel.is_file():
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100218 convert_numpy_file(
219 output_type, ofm_refmodel, output_dir, outstem=cfm
220 )
Jeremy Johnson6179c212022-01-13 13:46:35 +0000221 else:
222 logger.error(f"Missing result file {ofm_path}")
223 return None
Jeremy Johnson1271c442023-09-05 11:39:26 +0100224 cfm_files.append(cfm + ".npy")
Jeremy Johnson6179c212022-01-13 13:46:35 +0000225 # Remove path and "ref-"/"ref_model_" from output filenames
226 ofm_files.append(strip_ref_output_name(ofm_path.name))
227
228 # Rewrite output file names as they can be relative, but keep them npys
229 test_desc["ofm_file"] = ofm_files
Jeremy Johnson1271c442023-09-05 11:39:26 +0100230 if not test_desc["expected_failure"] and cfm_files:
231 # Output expected result file for conformance if expected pass and we
232 # have some files!
Jeremy Johnson6179c212022-01-13 13:46:35 +0000233 test_desc["expected_result_file"] = cfm_files
Jeremy Johnson88588622022-07-12 16:42:29 +0100234
235 # Add supported profiles
236 if profiles is None:
237 # Assume base profile
238 profiles = [PROFILES_LIST[0]]
239 test_desc["profile"] = profiles
240
Jeremy Johnsonfd05bb32023-02-07 16:39:24 +0000241 # Add tags (if any)
242 if tags is not None:
evacha01ad8e1e22024-03-19 12:42:17 +0000243 test_desc["tag"] = test_desc.get("tag", []) + tags
Jeremy Johnsonfd05bb32023-02-07 16:39:24 +0000244
Jeremy Johnson6179c212022-01-13 13:46:35 +0000245 return test_desc
246
247
248def strip_ref_output_name(name):
249 """Remove mentions of reference from output files."""
250 if name.startswith("ref-"):
251 name = name[4:]
252 if name.startswith("ref_model_"):
253 name = name[10:]
254 return name
255
256
257def main(argv=None):
258 """Convert the given directory to a conformance test."""
259 args = parse_args(argv)
260 # Verbosity
261 if args.verbose:
262 logger.setLevel(logging.DEBUG)
263
264 # Check we can get the files we need
Jeremy Johnsonf0348ea2023-09-27 16:10:59 +0100265 if not args.flatc_path.is_file():
266 logger.error("flatc not found at %s", args.flatc_path)
267 return 2
268 if not args.schema_path.is_file():
269 logger.error("TOSA schema not found at %s", args.schema_path)
Jeremy Johnson6179c212022-01-13 13:46:35 +0000270 return 2
271
272 # Work out where the desc.json file is
273 desc_filename = args.test_dir / NAME_DESC_FILENAME
274 framework_conversion = False
Jeremy Johnson0ecfa372022-06-30 14:27:56 +0100275 test_type_desc = "unknown"
Jeremy Johnson6179c212022-01-13 13:46:35 +0000276 if desc_filename.is_file():
Jeremy Johnson0ecfa372022-06-30 14:27:56 +0100277 logger.debug("Found TOSA operator unit test")
278 test_type_desc = "TOSA operator"
Jeremy Johnson6179c212022-01-13 13:46:35 +0000279 else:
280 desc_filename = (
281 args.test_dir
282 / get_framework_name(NAME_FLATBUFFER_DIR, args.framework)
283 / NAME_DESC_FILENAME
284 )
285 if desc_filename.is_file():
Jeremy Johnson0ecfa372022-06-30 14:27:56 +0100286 logger.debug(f"Found framework unit test for {args.framework}")
287 test_type_desc = f"{args.framework}"
Jeremy Johnson6179c212022-01-13 13:46:35 +0000288 framework_conversion = True
289 else:
290 logger.error(f"Could not find {NAME_DESC_FILENAME} in {args.test_dir}")
291 return 2
292 logger.debug(f"desc.json file: {desc_filename}")
293
294 # Check for required files for framework conversion
295 if framework_conversion:
296 try:
297 framework_schema, framework_filename = find_framework_artifacts(
298 args.framework, args.framework_schema, desc_filename
299 )
300 except Exception as err:
301 logger.error(err)
302 return 2
303 else:
304 framework_schema, framework_filename = None, None
305
306 # Open the meta desc.json file
307 with open(desc_filename, mode="r") as fd:
308 test_desc = json.load(fd)
309
310 if "tosa_file" not in test_desc:
311 logger.error(f"Unsupported desc.json file found {desc_filename}")
312 return 2
313
314 # Dictionary fix
315 if "ifm_name" not in test_desc:
316 logger.warn("Old format desc.json file found - attempting to fix up")
317 test_desc["ifm_name"] = test_desc["ifm_placeholder"]
318 del test_desc["ifm_placeholder"]
319
320 # Make the output directory if needed
321 try:
Jeremy Johnsondd8d9c22022-12-12 14:18:10 +0000322 args.output_dir.mkdir(parents=True, exist_ok=(not args.strict))
Jeremy Johnson6179c212022-01-13 13:46:35 +0000323 except FileExistsError:
Jeremy Johnsondd8d9c22022-12-12 14:18:10 +0000324 if args.strict:
325 logger.error(f"{args.output_dir} already exists")
326 else:
327 logger.error(f"{args.output_dir} is not a directory")
Jeremy Johnson6179c212022-01-13 13:46:35 +0000328 return 2
329
330 # Convert the TOSA flatbuffer binary
331 tosa_filename = desc_filename.parent / test_desc["tosa_file"]
332 tosa_filename = convert_flatbuffer_file(
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100333 args.output_type,
334 args.flatc_path,
335 args.schema_path,
336 tosa_filename,
337 args.output_dir,
Jeremy Johnson6179c212022-01-13 13:46:35 +0000338 )
339 if not tosa_filename:
340 # Failed to convert the file, json2fbbin will have printed an error
341 return 1
342 else:
343 # Replace binary with JSON name
344 test_desc["tosa_file"] = tosa_filename.name
345
346 if framework_conversion and framework_filename:
347 # Convert the framework flatbuffer binary
348 framework_filename = convert_flatbuffer_file(
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100349 args.output_type,
350 args.flatc_path,
351 framework_schema,
352 framework_filename,
353 args.output_dir,
Jeremy Johnson6179c212022-01-13 13:46:35 +0000354 )
355 if not framework_filename:
356 # Failed to convert the file, json2fbbin will have printed an error
357 return 1
358
359 # Convert input files to JSON
360 ifm_files = []
361 for file in test_desc["ifm_file"]:
Jeremy Johnson1271c442023-09-05 11:39:26 +0100362 if file:
Jeremy Johnson6179c212022-01-13 13:46:35 +0000363 path = desc_filename.parent / file
Jeremy Johnson6179c212022-01-13 13:46:35 +0000364 ifm_files.append(path.name)
Jeremy Johnson1271c442023-09-05 11:39:26 +0100365 if path.is_file():
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100366 convert_numpy_file(args.output_type, path, args.output_dir)
Jeremy Johnson1271c442023-09-05 11:39:26 +0100367 else:
368 if not args.lazy_data_generation:
369 logger.error(f"Missing input file {path.name}")
370 return 1
371
Jeremy Johnson6179c212022-01-13 13:46:35 +0000372 # Rewrite input file names to make sure the paths are correct,
373 # but keep them numpys as the test runner will convert them back
374 # before giving them to the SUT
375 test_desc["ifm_file"] = ifm_files
376
Jeremy Johnson1271c442023-09-05 11:39:26 +0100377 # Check for cpp files for data-generator/verifier
378 cpp_files = args.test_dir.glob("*.cpp")
379 for cpp in cpp_files:
380 shutil.copy(str(cpp), str(args.output_dir))
381
Jeremy Johnsone2b5e872023-09-14 17:02:09 +0100382 # Work out if we have a result to record
383 record_result = not args.lazy_data_generation
384 if "meta" in test_desc and "compliance" in test_desc["meta"]:
385 # We don't have pre-generated results for compliance tests
386 record_result = False
387
Jeremy Johnson6179c212022-01-13 13:46:35 +0000388 # Update desc.json and convert result files to JSON
389 test_desc = update_desc_json(
Jeremy Johnson9c2fe6e2023-10-04 16:55:04 +0100390 args.output_type,
Jeremy Johnson88588622022-07-12 16:42:29 +0100391 desc_filename.parent,
392 test_desc,
393 output_dir=args.output_dir,
Jeremy Johnsone2b5e872023-09-14 17:02:09 +0100394 record_result=record_result,
Jeremy Johnson88588622022-07-12 16:42:29 +0100395 profiles=args.profile,
Jeremy Johnson76c6a552023-09-11 09:30:02 +0100396 tags=args.tags,
Jeremy Johnson6179c212022-01-13 13:46:35 +0000397 )
398 if not test_desc:
399 # Error from conversion/update
400 return 1
401
Jeremy Johnson1271c442023-09-05 11:39:26 +0100402 # Validate the desc.json schema
Jeremy Johnson76c6a552023-09-11 09:30:02 +0100403 try:
404 TestDescSchemaValidator().validate_config(test_desc)
405 except Exception as e:
406 logger.error(e)
407 return 1
Jeremy Johnson1271c442023-09-05 11:39:26 +0100408
Jeremy Johnson6179c212022-01-13 13:46:35 +0000409 # Output new desc.json
410 new_desc_filename = args.output_dir / NAME_DESC_FILENAME
411 with open(new_desc_filename, "w") as fd:
412 json.dump(test_desc, fd, indent=2)
413
Jeremy Johnson0ecfa372022-06-30 14:27:56 +0100414 logger.info(f"Converted {test_type_desc} test to {args.output_dir}")
Jeremy Johnson6179c212022-01-13 13:46:35 +0000415 return 0
416
417
418if __name__ == "__main__":
419 exit(main())