-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
492 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
|
||
"""CI script to test the reference implementation for a particular chapter. | ||
Test all combinations of extra credit features and other test options. | ||
This script's main purpose is to test the test suite itself (e.g. make sure | ||
we didn't accidentally use Part II features in what's supposed to be a | ||
Part I-only test). | ||
We do exhaustive tests of combinations only on Linux; on macOS we run | ||
all the tests (including extra-credit tests) but don't run tests of | ||
intermediate stages or different combinations of extra credit features. | ||
""" | ||
import argparse | ||
import json | ||
import platform | ||
import subprocess | ||
|
||
# usage: /run_tests.py /path/to/cc --chapter 1 | ||
# for pre-coalescing chapter 20: ./run_tests.py /path/to/cc --chapter 1 --no-coalesce | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument( | ||
"cc", type=str, nargs="?", default=None, help="Path to compiler under test" | ||
) | ||
parser.add_argument( | ||
"--chapter", | ||
type=str, | ||
help=("Chapter whose implementation we're testing"), | ||
) | ||
|
||
parser.add_argument( | ||
"--skip-extra-credit", action="store_true", help="Don't run extra credit tests" | ||
) | ||
|
||
args = parser.parse_args() | ||
|
||
CC: str = args.cc | ||
CHAPTER: int | ||
NO_COALESCING: bool = False | ||
if args.chapter == "20a": | ||
CHAPTER = 20 | ||
NO_COALESCING = True | ||
else: | ||
CHAPTER = int(args.chapter) | ||
|
||
if CHAPTER < 1 or CHAPTER > 20: | ||
exit("Bad chapter number") | ||
|
||
# do we need rosetta? | ||
ARCH_PREFIX = "" | ||
if platform.machine().lower() == "arm64": | ||
ARCH_PREFIX = "arch -x86_64 " | ||
|
||
# figure out what combinations of extra-credit flags to test. Include: | ||
# --extra-credit (all extra credit features that have been implemented so far) | ||
# none (no extra credit features) | ||
# each combo of extra-credit features covered by any test case in this | ||
# chapter or an earlier one | ||
EXTRA_CRED_CHAPTERS = { | ||
"bitwise": 3, | ||
"compound": 5, | ||
"increment": 5, | ||
"goto": 6, | ||
"switch": 8, | ||
"nan": 13, | ||
"union": 18, | ||
} | ||
|
||
|
||
def get_idx(flag: str) -> tuple[int, str]: | ||
"""Sort flags by chapter, use alphabetical order as tiebreaker | ||
Just for consistency/determinism in test runs | ||
""" | ||
return (EXTRA_CRED_CHAPTERS[flag], flag) | ||
|
||
|
||
# all extra credit chapters implemented so far | ||
ALL_EXTRA_CREDIT: tuple = tuple( | ||
sorted( | ||
[f for f in EXTRA_CRED_CHAPTERS if EXTRA_CRED_CHAPTERS[f] <= CHAPTER], | ||
key=get_idx, | ||
) | ||
) | ||
|
||
# find set of all extra-credit flag combinations we should test, | ||
# based on test_properties.json, and record appropriate option | ||
# strings to enable each of them (including options passed to test_compiler | ||
# and options passed to the compiler itself | ||
extra_credit_combos: dict[tuple, dict[str, str]] | ||
|
||
if args.skip_extra_credit: | ||
extra_credit_combos = { | ||
# none (empty set) | ||
(): {"test_opts": "", "compiler_opts": ""}, | ||
} | ||
elif platform.system() == "Darwin": | ||
# only run full --extra-credit option | ||
extra_credit_combos = { | ||
# all | ||
ALL_EXTRA_CREDIT: { | ||
"test_opts": "--extra-credit", | ||
"compiler_opts": " ".join("--" + f for f in ALL_EXTRA_CREDIT), | ||
}, | ||
} | ||
else: | ||
extra_credit_combos = { | ||
# none (empty set) | ||
(): {"test_opts": "", "compiler_opts": ""}, | ||
# all | ||
ALL_EXTRA_CREDIT: { | ||
"test_opts": "--extra-credit", | ||
"compiler_opts": " ".join("--" + f for f in ALL_EXTRA_CREDIT), | ||
}, | ||
} | ||
# if the key-value pair "test/case.c": (flag1, flag2, ...) | ||
# appears in test_properties.json, and test/case.c is in the current | ||
# chapter or earlier, add (flag1, flag2, ...) to our set of extra credit combos | ||
with open("./test_properties.json", encoding="utf-8") as f: | ||
extra_cred_flags = json.load(f)["extra_credit_tests"] | ||
for path, flags in extra_cred_flags.items(): | ||
# all keys in this dict start with chapter_N/ | ||
chapter_num = int(path.split("/")[0].removeprefix("chapter_")) | ||
if chapter_num <= CHAPTER: | ||
k = tuple(sorted(flags, key=get_idx)) | ||
opt_string = " ".join("--" + flag for flag in k) | ||
extra_credit_combos[k] = { | ||
"test_opts": opt_string, | ||
"compiler_opts": opt_string, | ||
} | ||
|
||
|
||
# test functions | ||
def run_tests(test_script_opts: str = "", cc_opts: str = "") -> None: | ||
"""Invoke test_compiler script with a particular set of options. | ||
Args: | ||
test_script_opts: options to pass test_compiler | ||
cc_opts: options to pass the compiler under test (after -- ) | ||
""" | ||
|
||
if cc_opts: | ||
cc_opt_str = f" -- {cc_opts}" | ||
else: | ||
cc_opt_str = "" | ||
|
||
cmd = f"{ARCH_PREFIX}./test_compiler {CC} --chapter {CHAPTER} {test_script_opts}{cc_opt_str}" | ||
print(cmd, flush=True) | ||
subprocess.run( | ||
cmd, | ||
shell=True, | ||
check=True, | ||
) | ||
|
||
|
||
def test_normal() -> None: | ||
"""Main test method for if we're testing a chapter from part I or II""" | ||
# define the stages we're going to test | ||
stages: list[str] = ["lex", "parse", "tacky", "validate", "codegen", "run"] | ||
if platform.system() == "Darwin": | ||
# don't test intermediate stages | ||
stages = ["run"] | ||
elif CHAPTER == 1: | ||
# tacky and validate stages not added yet | ||
stages.remove("tacky") | ||
stages.remove("validate") | ||
elif CHAPTER < 5: | ||
# TACKY stage added, validate not | ||
stages.remove("validate") | ||
for _, extra_cred_opts in extra_credit_combos.items(): | ||
test_script_opts = extra_cred_opts["test_opts"] | ||
compiler_opts = extra_cred_opts["compiler_opts"] | ||
for stage in stages: | ||
stage_test_script_opts = f"{test_script_opts} --stage {stage}" | ||
run_tests(test_script_opts=stage_test_script_opts, cc_opts=compiler_opts) | ||
|
||
|
||
def test_chapter_19() -> None: | ||
"""Run tests for chapter 19. | ||
Don't test different stages but do run both with and without | ||
--int-only option | ||
""" | ||
for _, extra_cred_opts in extra_credit_combos.items(): | ||
test_script_opts = extra_cred_opts["test_opts"] | ||
compiler_opts = extra_cred_opts["compiler_opts"] | ||
# run with --int-only option | ||
run_tests( | ||
test_script_opts=test_script_opts + " --int-only", | ||
cc_opts=compiler_opts + " --int-only", | ||
) | ||
# run without it | ||
run_tests(test_script_opts=test_script_opts, cc_opts=compiler_opts) | ||
|
||
|
||
def test_chapter_20a() -> None: | ||
"""Run tests for chapter 20 without coalescing. | ||
Don't test different stages but do run both with and without | ||
--int-only option. | ||
""" | ||
for _, extra_cred_opts in extra_credit_combos.items(): | ||
test_script_opts = extra_cred_opts["test_opts"] | ||
compiler_opts = extra_cred_opts["compiler_opts"] | ||
# run with --int-only option | ||
run_tests( | ||
test_script_opts=test_script_opts + " --int-only --no-coalescing", | ||
cc_opts=compiler_opts + " --int-only", | ||
) | ||
# run without it | ||
run_tests( | ||
test_script_opts=test_script_opts + " --no-coalescing", | ||
cc_opts=compiler_opts, | ||
) | ||
|
||
|
||
def test_chapter_20() -> None: | ||
"""Run tests for chapter 20 with coalescing. | ||
Don't test different stages but do run both with and without | ||
--int-only option. | ||
""" | ||
for _, extra_cred_opts in extra_credit_combos.items(): | ||
test_script_opts = extra_cred_opts["test_opts"] | ||
compiler_opts = extra_cred_opts["compiler_opts"] | ||
# run with --int-only option | ||
run_tests( | ||
test_script_opts=test_script_opts + " --int-only", | ||
cc_opts=compiler_opts + " --int-only", | ||
) | ||
# run without it | ||
run_tests(test_script_opts=test_script_opts, cc_opts=compiler_opts) | ||
|
||
|
||
if __name__ == "__main__": | ||
if CHAPTER < 19: | ||
test_normal() | ||
elif CHAPTER == 19: | ||
test_chapter_19() | ||
elif CHAPTER == 20: | ||
if NO_COALESCING: | ||
test_chapter_20a() | ||
else: | ||
test_chapter_20() | ||
else: | ||
exit(f"Bad chapter number {CHAPTER}") | ||
exit(0) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# Build tagged versions of the reference implementation compiler | ||
name: Build the compiler | ||
|
||
on: | ||
workflow_dispatch: | ||
inputs: | ||
chapter: | ||
required: true | ||
type: string | ||
os: | ||
required: true | ||
default: ubuntu-latest | ||
|
||
workflow_call: | ||
inputs: | ||
chapter: | ||
required: true | ||
type: string | ||
os: | ||
type: string | ||
required: true | ||
default: ubuntu-latest | ||
branch: | ||
type: string | ||
required: false | ||
default: main | ||
|
||
env: | ||
EXE_DIR: _build/default/bin/ | ||
EXE_PATH: _build/default/bin/main.exe | ||
|
||
jobs: | ||
|
||
build: | ||
runs-on: ${{ inputs.os }} | ||
env: | ||
CHAPTER: ${{ inputs.chapter }} | ||
|
||
steps: | ||
|
||
# first check out at specified branch so we can find commit hash for this chapter | ||
- uses: actions/checkout@v4 | ||
with: | ||
repository: nlsandler/nqcc2 | ||
ref: ${{ inputs.branch }} | ||
fetch-depth: 0 # need this to get commit history | ||
|
||
# NOTE: for chapters with extra-credit features we have two commits: | ||
# one for regular feature, one for extra credit. The '-n 1' options limits | ||
# us to the first (i.e. later i.e. extra credit) commit hash | ||
- name: Get commit hash | ||
run: | | ||
git log --grep "chapter $CHAPTER\b" -i --format='%H' -n 1 | ||
commit=$(git log --grep "chapter $CHAPTER\b" -i --format='%H' -n 1) | ||
echo "commit=$commit" >> $GITHUB_ENV | ||
- name: Check out NQCC at chapter ${{ inputs.chapter }} | ||
uses: actions/checkout@v4 | ||
with: | ||
repository: nlsandler/nqcc2 | ||
ref: ${{ env.commit }} | ||
|
||
- name: Construct cache key | ||
id: make-key | ||
env: | ||
runner_os: ${{ runner.os }} | ||
run: | | ||
commit=$(git rev-parse --short "$commit") | ||
echo "cache-key=${runner_os}-${commit}-nqcc" >> $GITHUB_OUTPUT | ||
- name: Cache build result | ||
id: cache-nqcc | ||
uses: actions/cache@v4 | ||
with: | ||
path: ${{ env.EXE_PATH }} | ||
key: ${{ steps.make-key.outputs.cache-key }} | ||
|
||
# skip building if we get a cache hit | ||
- name: Set up OCaml | ||
if: steps.cache-nqcc.outputs.cache-hit != 'true' | ||
uses: ocaml/setup-ocaml@v3 | ||
with: | ||
ocaml-compiler: 5.2.x | ||
# necessary to avoid random errors, see https://github.com/ocaml/setup-ocaml/issues/400 | ||
dune-cache: false | ||
|
||
- name: Build it | ||
if: steps.cache-nqcc.outputs.cache-hit != 'true' | ||
run: | | ||
opam install . --deps-only | ||
opam exec -- dune build | ||
- name: Upload binary | ||
uses: actions/upload-artifact@v4 | ||
with: | ||
name: nqcc-${{inputs.os}}-${{ inputs.chapter }} | ||
path: ${{ env.EXE_PATH }} |
Oops, something went wrong.