Skip to content

Commit

Permalink
Finalizes local testing set-up
Browse files Browse the repository at this point in the history
Local tests are run using:
```
pixi run test local <node1> <node2> <...>
```
You can check the status of the local test run using:
```
pixi run test status
```
Finally, you MUST sign off on the current version:
```
pixi run test sign
```
The GitHub actions tests will check that the signed version is current.
  • Loading branch information
avcopan committed Nov 25, 2024
1 parent ea6fa0c commit 5489cff
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 82 deletions.
2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ download = { cmd = "./scripts/download.sh", cwd = "." }
install = { cmd = "./scripts/install.sh", cwd = "." }
update = { cmd = "./scripts/update.sh", cwd = "." }
git = { cmd = "./scripts/git.sh", cwd = "." }
test = { cmd = "./scripts/test.py", cwd = "." }
test = { cmd = "./scripts/test.py" }
subtasks = { cmd = "./scripts/subtasks.sh" }
node = { cmd = "./scripts/node.sh" }
x2z = { cmd = "./scripts/x2z.py" }
Expand Down
211 changes: 130 additions & 81 deletions scripts/test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python
import os
import re
import shutil
import subprocess
import tarfile
import textwrap
import warnings
from collections.abc import Sequence
from pathlib import Path
Expand All @@ -14,23 +14,35 @@
from pydantic import BaseModel


class RepoInfo(BaseModel, extra="allow"):
autochem: str | list[str]
autoio: str | list[str]
autofile: str | list[str]
mechanalyzer: str | list[str]
mechdriver: str | list[str]
class Provenance(BaseModel):
autochem: str
autoio: str
autofile: str
mechanalyzer: str
mechdriver: str


SRC_DIR = Path("src").resolve()
EXAMPLE_ROOT_DIR = Path("src/mechdriver/examples").resolve()
TEST_ROOT_DIR = Path("src/mechdriver/tests").resolve()
class Signature(BaseModel):
provenance: Provenance
overrides: dict[str, list[str]]
username: str


COMMIT_MESSAGE = "Updates tests/archive.tgz"

os.chdir(os.environ.get("INIT_CWD"))

ROOT_DIR = Path(os.environ.get("PIXI_PROJECT_ROOT")).resolve()
SRC_DIR = ROOT_DIR / "src"
MECHDRIVER_DIR = SRC_DIR / "mechdriver"
EXAMPLE_ROOT_DIR = MECHDRIVER_DIR / "examples"
TEST_ROOT_DIR = MECHDRIVER_DIR / "tests"
TESTS = [t for t in yaml.safe_load((TEST_ROOT_DIR / "config.yaml").read_text())]
EXAMPLE_DIRS: list[Path] = [EXAMPLE_ROOT_DIR / t for t in TESTS]
TEST_DIRS: list[Path] = [TEST_ROOT_DIR / t for t in TESTS]

RUN_PROV_FILE = TEST_ROOT_DIR / "run_prov.yaml"
SIGN_PROV_FILE = TEST_ROOT_DIR / "sign_prov.yaml"
PROVENANCE_FILE = TEST_ROOT_DIR / "provenance.yaml"
SIGNATURE_FILE = TEST_ROOT_DIR / "signature.yaml"
ARCHIVE_FILE = TEST_ROOT_DIR / "archive.tgz"


Expand All @@ -40,6 +52,15 @@ def main():
pass


@main.command("status")
def status():
"""Check the status of local tests."""
for test in TESTS:
test_dir = TEST_ROOT_DIR / test
print(f"Checking status in {test_dir}...")
automech.subtasks.status(test_dir, check_file=f"check_{test}.log")


@main.command("local")
@click.argument("nodes", nargs=-1)
def local(nodes: Sequence[str]):
Expand All @@ -51,9 +72,9 @@ def local(nodes: Sequence[str]):

# 1. Record the current repository versions
check_for_uncommited_python_changes(throw_error=True)
run_prov = repos_current_version()
print(f"\nWriting setup repo information to {RUN_PROV_FILE}")
RUN_PROV_FILE.write_text(yaml.safe_dump(dict(run_prov)))
prov = repos_current_version()
print(f"\nWriting setup repo information to {PROVENANCE_FILE}")
PROVENANCE_FILE.write_text(yaml.safe_dump(prov.model_dump()))

# 2. Copy the example directories over
for test_dir, example_dir in zip(TEST_DIRS, EXAMPLE_DIRS, strict=True):
Expand All @@ -72,51 +93,119 @@ def local(nodes: Sequence[str]):
TEST_DIRS, nodes=nodes, activation_hook=pixi_activation_hook()
)


@main.command("status")
def status():
"""Check the status of electronic structure calculations."""
for test in TESTS:
test_dir = TEST_ROOT_DIR / test.name
print(f"Checking status in {test_dir}...")
os.chdir(test_dir)
automech.subtasks.status()
# 5. Archive the results
archive()


@main.command("sign")
def sign():
"""Sign off on electronic structure calculations."""
setup_repo_info = RepoInfo(**yaml.safe_load(RUN_PROV_FILE.read_text()))
signed_repo_info = repos_version_diff(setup_repo_info)
print(f"\nWriting signed repo information to {SIGN_PROV_FILE}")
signed_dct = {"username": github_username(), **dict(signed_repo_info)}
SIGN_PROV_FILE.write_text(yaml.safe_dump(signed_dct))
"""Sign off on local tests."""
check_for_uncommited_python_changes()

# Unpack archive
unpack_archive()

# Compare to tested version and prompt for override as needed
prov0 = Provenance(**yaml.safe_load(PROVENANCE_FILE.read_text()))
diff_dct = repos_version_diff(prov0)
overrides = {}
print("This assumes that you first ran local tests using `pixi run test local`")
print("Checking that the tests were run against the current repository versions...")
for repo, diffs in diff_dct.items():
print(
f"WARNING: {repo} does not match the tested version!! "
f"It has {len(diffs)} additional commits:"
)
print(textwrap.indent("\n".join(diffs), " "))
answer = input(
"Do you solemnly swear that these changes will not affect the tests? (yes/no): "
)
print()
if answer == "yes":
overrides[repo] = diffs
else:
print("Thank you for your honesty.")
print("Please re-run the tests using `pixi run test local`.")
return

# Sign
signature = Signature(
provenance=repos_current_version(),
overrides=overrides,
username=github_username(),
)
print(f"\nWriting signed repo information to {SIGNATURE_FILE}")
SIGNATURE_FILE.write_text(yaml.safe_dump(signature.model_dump()))

# Archive
archive()

# Commit the archive
subprocess.run(["git", "restore", "--staged", "."], cwd=MECHDRIVER_DIR)
subprocess.run(["git", "add", str(ARCHIVE_FILE)], cwd=MECHDRIVER_DIR)
subprocess.run(["git", "commit", "-m", COMMIT_MESSAGE], cwd=MECHDRIVER_DIR)


def archive() -> None:
"""Tar a directory in its current location
:param path: The path to a directory
"""
exclude = ("subtasks", "run")

def _filter(obj: tarfile.TarInfo) -> tarfile.TarInfo | None:
"""Filter function for excluding unneeded directories."""
name = obj.name
if any(f"{e}/" in name or name.endswith(e) for e in exclude):
return None
return obj

os.chdir(TEST_ROOT_DIR)
print(f"Creating {ARCHIVE_FILE}...")
ARCHIVE_FILE.unlink(missing_ok=True)
with tarfile.open(ARCHIVE_FILE, "w:gz") as tar:
if PROVENANCE_FILE.exists():
tar.add(PROVENANCE_FILE, arcname=PROVENANCE_FILE.name)
if SIGNATURE_FILE.exists():
tar.add(SIGNATURE_FILE, arcname=SIGNATURE_FILE.name)
for test in TESTS:
tar.add(test, arcname=test, filter=_filter)


def unpack_archive() -> None:
"""Un-tar a directory in its current location
def repos_current_version() -> RepoInfo:
:param path: The path where the AutoMech subtasks were set up
"""
os.chdir(TEST_ROOT_DIR)
if ARCHIVE_FILE.exists():
print(f"Unpacking {ARCHIVE_FILE}...")
with tarfile.open(ARCHIVE_FILE, "r") as tar:
tar.extractall()


def repos_current_version() -> Provenance:
"""Get information about the current version of each repo
:return: One-line summaries of most recent commits
"""
check_for_uncommited_python_changes()
version_dct = repos_output(["git", "log", "--oneline", "-n", "1"])
return RepoInfo(**version_dct)
return Provenance(**version_dct)


def repos_version_diff(repo_info: RepoInfo) -> RepoInfo:
def repos_version_diff(repo_info: Provenance) -> dict:
"""Get information about differences in the current version of each repo, relative
to a past version
:param repo_info: The repository information to compare to
:return: One-line summaries of each commit since the one given
"""
check_for_uncommited_python_changes()
command_dct = {
k: [f"{commit_hash_from_line(v)}..HEAD"] for k, v in dict(repo_info).items()
}
diff_dct = repos_output(["git", "log", "--oneline"], command_dct=command_dct)
diff_dct = {k: v.splitlines() for k, v in diff_dct.items()}
return RepoInfo(**diff_dct)
diff_dct = {k: v.splitlines() for k, v in diff_dct.items() if v}
return diff_dct


class UncommittedChangesError(Exception):
Expand All @@ -136,11 +225,9 @@ def check_for_uncommited_python_changes(throw_error: bool = False) -> None:
status_dct = repos_output(["git", "status", "-s", "*.py"])
for repo, status in status_dct.items():
if status:
message = (
f"{repo} has uncommitted Python changes:\n{status}\n"
f"Make sure your repositories are clean before running tests!\n"
)
message = f"{repo} has uncommitted Python changes:\n{status}\n"
if throw_error:
message += "Please commit changes before running tests!\n"
raise UncommittedChangesError(message)
else:
warnings.warn(message)
Expand All @@ -166,7 +253,7 @@ def repos_output(
"""
command_dct = {} if command_dct is None else command_dct
output_dct = {}
for repo in RepoInfo.model_fields:
for repo in Provenance.model_fields:
repo_dir = SRC_DIR / repo
full_command = command + command_dct.get(repo, [])
output_dct[repo] = subprocess.check_output(
Expand All @@ -190,45 +277,7 @@ def github_username() -> str:


def pixi_activation_hook() -> str:
result = subprocess.run(["pixi", "shell-hook"], capture_output=True, text=True)
return result.stdout


@main.command("archive")
def archive() -> None:
"""Tar a directory in its current location
:param path: The path to a directory
"""
exclude = ("subtasks/", "run/")

os.chdir(TEST_ROOT_DIR)
print(f"Creating {ARCHIVE_FILE}...")
ARCHIVE_FILE.unlink(missing_ok=True)
with tarfile.open(ARCHIVE_FILE, "w:gz") as tar:
if RUN_PROV_FILE.exists():
tar.add(RUN_PROV_FILE, arcname=RUN_PROV_FILE.name)
if SIGN_PROV_FILE.exists():
tar.add(SIGN_PROV_FILE, arcname=SIGN_PROV_FILE.name)
for test in TESTS:
tar.add(
test,
arcname=test,
filter=lambda x: None if any(e in x.name for e in exclude) else x,
)


@main.command("unpack")
def unpack_archive() -> None:
"""Un-tar a directory in its current location
:param path: The path where the AutoMech subtasks were set up
"""
os.chdir(TEST_ROOT_DIR)
if ARCHIVE_FILE.exists():
print(f"Unpacking {ARCHIVE_FILE}...")
with tarfile.open(ARCHIVE_FILE, "r") as tar:
tar.extractall()
return subprocess.check_output(["pixi", "shell-hook"], text=True)


if __name__ == "__main__":
Expand Down

0 comments on commit 5489cff

Please sign in to comment.