From 9f38b041a67727aca75313cc3f9271f68da9fb50 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 29 Jul 2025 15:48:10 +0100 Subject: [PATCH] Create script to zip up picamera tests and hashes of releavant source files --- hardware-specific-tests/README.md | 23 ++++++ picamera_tests.py | 120 ++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 hardware-specific-tests/README.md create mode 100755 picamera_tests.py diff --git a/hardware-specific-tests/README.md b/hardware-specific-tests/README.md new file mode 100644 index 00000000..62172610 --- /dev/null +++ b/hardware-specific-tests/README.md @@ -0,0 +1,23 @@ +## Hardware specific tests for the PiCamera. + +These are very slow as they run on hardware. They can be run with: + + pytest hardware-specific-tests + +It is essential to stop the server first: + + ofm stop + +However, this will not archive the tests in the Git repository for reporting the coverage. For this see the section below on reporting the coverage. + +When writing and debugging these unit tests it is often best to run a specific test and to use the `-s` flag to see the print statments. It is also often useful to use `--pdb` to drop you into a python debug session on any failure. For example, you might run: + + /picamera2/test_exposure_time_drift.py::test_exposure_time_saves_and_loads -s --pdb + +### Reporting the coverage + +To create a coverage report that will be included into the repository run: + + ./picamera_tests.py run + +This will create a zip of test results along with hashes of the picamera files and the base camera file. The server will include this overage report if the source files remain the same. \ No newline at end of file diff --git a/picamera_tests.py b/picamera_tests.py new file mode 100755 index 00000000..da648d4e --- /dev/null +++ b/picamera_tests.py @@ -0,0 +1,120 @@ +#! /usr/bin/env python3 +"""Run the Piamera tests and archive the results in the git repository.""" + +import subprocess +import shutil +import os +import posixpath +import argparse +import json +import sys +import zipfile + +CAM_DIR = os.path.join("src", "openflexure_microscope_server", "things", "camera") +ZIP_FILE = "picamera_coverage.zip" +COVERAGE_FILE = ".coverage.picamera" +HASH_FILE = "picamera_test_hashes.json" + +# If any of the HASHED_FILES change, then the picamera tests need to be re-run. +# System independent can be checked on windows even if running tests must be on a Pi +HASHED_FILES = [ + os.path.join(CAM_DIR, "__init__.py"), + os.path.join(CAM_DIR, "picamera.py"), + os.path.join(CAM_DIR, "picamera_recalibrate_utils.py"), +] + + +def _get_hashes(include_coverage=False): + """Return a dictonary of Git hashes for the HASHED FILES. + + The key is the posixpath to the file so matching works even if the dictionary is + compared to a dictionary created on another system. + + The coverage file is hashed when running for debug purposes. + + Git hashes are so the files are line ending indpendent. + """ + hashes = {} + filepaths = HASHED_FILES + if include_coverage: + filepaths.append(COVERAGE_FILE) + for filepath in HASHED_FILES: + if not os.path.isfile(filepath): + # Sys exit rather than raise for better command line experience + print(f"ERROR: {filepath} does not exist!") + sys.exit(-1) + # Create git hashes for file system independence + file_hash = subprocess.check_output(["git", "hash-object", filepath]) + hashes[posixpath.normpath(filepath)] = file_hash.decode("utf-8").strip() + return hashes + + +def run_tests(): + """Run the picamera tests, zip the coverage database, hash relevant source files.""" + subprocess.run(["pytest", "hardware-specific-tests"], check=True) + shutil.copyfile(".coverage", COVERAGE_FILE) + hash_dict = _get_hashes(include_coverage=True) + with open(HASH_FILE, "w", encoding="utf-8") as json_file: + json.dump(hash_dict, json_file) + with zipfile.ZipFile(ZIP_FILE, "w") as zipf: + zipf.write(COVERAGE_FILE) + zipf.write(HASH_FILE) + + +def unzip_coverage(): + """Unzip coverage database if hashes match relevant source files.""" + if zipfile.is_zipfile(ZIP_FILE): + with zipfile.ZipFile(ZIP_FILE, "r") as zip_obj: + zip_obj.extractall() + else: + print("ERROR: Zipfile with coverage results not found.") + sys.exit(-1) + + hash_dict = _get_hashes() + + if os.path.isfile(HASH_FILE): + with open(HASH_FILE, "r", encoding="utf-8") as json_file: + zipped_hash_dict = json.load(json_file) + else: + print("ERROR: JSON hashes not available. Zipfile is incomplete.") + sys.exit(-1) + del zipped_hash_dict[COVERAGE_FILE] + + if hash_dict != zipped_hash_dict: + # Sys exit rather than raise for better command line experience + print( + "ERROR: hashes don't match. This probably means the source has changed " + "since the tests were last run on the Pi." + ) + sys.exit(-1) + + +def main(): + """Either run tests or unzip previous results.""" + parser = argparse.ArgumentParser( + description=( + "picamera_tests: run or validate PiCamera hardware test coverage.\n\n" + "Use 'run' on the Pi to execute tests and package coverage data.\n" + "Use 'unzip' on another machine to extract and verify that the test\n" + "data matches the source code before unzipping it." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + subparsers.add_parser("run", help="Run hardware-specific tests on the PiCamera") + + subparsers.add_parser( + "unzip", help="Unzip and verify coverage data on another machine" + ) + + args = parser.parse_args() + + if args.command == "run": + run_tests() + elif args.command == "unzip": + unzip_coverage() + + +if __name__ == "__main__": + main()