Skip to content

Commit de39087

Browse files
committed
Manifest: added proof runtimes
Recorded runtime details for all existing proofs Proofs running longer than 1 minute will not be run in CI Signed-off-by: Andrew Helwer <[email protected]>
1 parent 3c0c83b commit de39087

File tree

30 files changed

+284
-179
lines changed

30 files changed

+284
-179
lines changed

.github/scripts/check_manifest_features.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,19 @@ def check_features(parser, queries, manifest, examples_root):
164164
if parse_err:
165165
success = False
166166
logging.error(f'Module {module["path"]} contains syntax errors')
167-
expected_features = get_tree_features(tree, queries)
167+
module_features = get_tree_features(tree, queries)
168+
expected_features = module_features - {'proof'}
168169
actual_features = set(module['features'])
169170
if expected_features != actual_features:
170171
success = False
171172
logging.error(
172173
f'Module {module["path"]} has incorrect features in manifest; '
173174
+ f'expected {list(expected_features)}, actual {list(actual_features)}'
174175
)
175-
expected_imports = get_community_imports(examples_root, tree, text, dirname(module_path), 'proof' in expected_features, queries)
176+
if 'proof' in module_features and 'proof' not in module:
177+
success = False
178+
logging.error(f'Module {module["path"]} contains proof but no proof runtime details')
179+
expected_imports = get_community_imports(examples_root, tree, text, dirname(module_path), 'proof' in module_features, queries)
176180
actual_imports = set(module['communityDependencies'])
177181
if expected_imports != actual_imports:
178182
success = False

.github/scripts/check_markdown_table.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def from_json(path, spec):
6767
path,
6868
set(spec['authors']),
6969
'beginner' in spec['tags'],
70-
any([module for module in spec['modules'] if 'proof' in module['features']]),
70+
any([module for module in spec['modules'] if 'proof' in module]),
7171
any([module for module in spec['modules'] if 'pluscal' in module['features']]),
7272
any([model for module in spec['modules'] for model in module['models'] if model['mode'] != 'symbolic']),
7373
any([model for module in spec['modules'] for model in module['models'] if model['mode'] == 'symbolic']),

.github/scripts/check_proofs.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
"""
44

55
from argparse import ArgumentParser
6+
from datetime import timedelta
67
from os.path import dirname, join, normpath
78
import logging
89
import subprocess
910
from timeit import default_timer as timer
1011
import tla_utils
1112

1213
parser = ArgumentParser(description='Validate all proofs in all modules with TLAPM.')
13-
parser.add_argument('--tlapm_path', help='Path to TLAPM install dir; should have bin and lib subdirs', required=True)
14-
parser.add_argument('--examples_root', help='Root directory of the tlaplus/examples repository', required=True)
14+
parser.add_argument('--tlapm_path', help='Path to TLAPM install dir; should have bin and lib subdirs', required=False, default = 'deps/tlapm')
15+
parser.add_argument('--examples_root', help='Root directory of the tlaplus/examples repository', required=False, default='.')
16+
parser.add_argument('--runtime_seconds_limit', help='Only run proofs with expected runtime less than this value', required=False, default=60)
1517
parser.add_argument('--skip', nargs='+', help='Space-separated list of .tla modules to skip checking', required=False, default=[])
1618
parser.add_argument('--only', nargs='+', help='If provided, only check proofs in this space-separated list', required=False, default=[])
1719
parser.add_argument('--verbose', help='Set logging output level to debug', action='store_true')
@@ -22,46 +24,70 @@
2224
manifest = tla_utils.load_all_manifests(examples_root)
2325
skip_modules = args.skip
2426
only_modules = args.only
27+
hard_timeout_in_seconds = args.runtime_seconds_limit * 2
2528

2629
logging.basicConfig(level = logging.DEBUG if args.verbose else logging.INFO)
2730

28-
proof_module_paths = [
29-
module['path']
30-
for path, spec in manifest
31-
for module in spec['modules']
32-
if 'proof' in module['features']
33-
and module['path'] not in skip_modules
34-
and (only_modules == [] or module['path'] in only_modules)
35-
]
31+
proof_module_paths = sorted(
32+
[
33+
(manifest_dir, spec, module, runtime)
34+
for manifest_dir, spec in manifest
35+
for module in spec['modules']
36+
if 'proof' in module
37+
and (runtime := tla_utils.parse_timespan(module['proof']['runtime'])) < timedelta(seconds = args.runtime_seconds_limit)
38+
and module['path'] not in skip_modules
39+
and (only_modules == [] or module['path'] in only_modules)
40+
],
41+
key = lambda m : m[3]
42+
)
3643

3744
for path in skip_modules:
3845
logging.info(f'Skipping {path}')
3946

4047
success = True
4148
tlapm_path = join(tlapm_path, 'bin', 'tlapm')
42-
for module_path in proof_module_paths:
49+
for manifest_dir, spec, module, expected_runtime in proof_module_paths:
50+
module_path = module['path']
4351
logging.info(module_path)
4452
start_time = timer()
45-
module_path = tla_utils.from_cwd(examples_root, module_path)
46-
module_dir = dirname(module_path)
47-
tlapm = subprocess.run(
53+
full_module_path = tla_utils.from_cwd(examples_root, module_path)
54+
module_dir = dirname(full_module_path)
55+
tlapm_result = subprocess.run(
4856
[
49-
tlapm_path, module_path,
57+
tlapm_path, full_module_path,
5058
'-I', module_dir,
5159
'--stretch', '5'
5260
],
53-
stdout=subprocess.PIPE,
54-
stderr=subprocess.STDOUT,
55-
text=True
61+
stdout = subprocess.PIPE,
62+
stderr = subprocess.STDOUT,
63+
text = True,
64+
timeout = hard_timeout_in_seconds
5665
)
5766
end_time = timer()
58-
logging.info(f'Checked proofs in {end_time - start_time:.1f}s')
59-
if tlapm.returncode != 0:
60-
logging.error(f'Proof checking failed in {module_path}:')
61-
logging.error(tlapm.stdout)
62-
success = False
63-
else:
64-
logging.debug(tlapm.stdout)
67+
actual_runtime = timedelta(seconds = end_time - start_time)
68+
match tlapm_result:
69+
case subprocess.CompletedProcess():
70+
output = ' '.join(tlapm_result.args) + '\n' + tlapm_result.stdout
71+
logging.info(f'Checked proofs in {tla_utils.format_timespan(actual_runtime)} vs. {tla_utils.format_timespan(expected_runtime)} expected')
72+
if tlapm_result.returncode != 0:
73+
logging.error(f'Proof checking failed for {module_path}:')
74+
logging.error(output)
75+
success = False
76+
else:
77+
if 'proof' not in module or module['proof']['runtime'] == 'unknown':
78+
module['proof'] = { 'runtime' : tla_utils.format_timespan(actual_runtime) }
79+
manifest_path = join(manifest_dir, 'manifest.json')
80+
tla_utils.write_json(spec, manifest_path)
81+
logging.debug(output)
82+
case subprocess.TimeoutExpired():
83+
logging.error(f'{module_path} hit hard timeout of {hard_timeout_in_seconds} seconds')
84+
args, _ = tlapm_result.args
85+
output = ' '.join(args) + '\n' + tlapm_result.stdout
86+
logging.error(output)
87+
success = False
88+
case _:
89+
logging.error(f'Unhandled TLAPM result type {type(tlapm_result)}: {tlapm_result}')
90+
success = False
6591

6692
exit(0 if success else 1)
6793

.github/scripts/generate_manifest.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,20 @@ def get_tla_files(examples_root, dir_path):
3535
Gets paths of all .tla files in the given directory, except for error
3636
trace specs.
3737
"""
38-
return [
39-
path for path in glob.glob(f'{dir_path}/**/*.tla', root_dir=examples_root, recursive=True)
38+
return sorted(
39+
path
40+
for path in glob.glob(f'{dir_path}/**/*.tla', root_dir=examples_root, recursive=True)
4041
if '_TTrace_' not in path
42+
)
43+
44+
def get_tla_file_features(examples_root, dir_path, parser, queries):
45+
"""
46+
Gets paths of all .tla files in a given directory, along with their
47+
features.
48+
"""
49+
return [
50+
(path, get_module_features(examples_root, path, parser, queries))
51+
for path in get_tla_files(examples_root, dir_path)
4152
]
4253

4354
def get_cfg_files(examples_root, tla_path):
@@ -73,7 +84,7 @@ def generate_new_manifest(examples_root, spec_path, spec_name, parser, queries):
7384
{
7485
'path': tla_utils.to_posix(tla_path),
7586
'communityDependencies': sorted(list(get_community_module_imports(examples_root, parser, tla_path, queries))),
76-
'features': sorted(list(get_module_features(examples_root, tla_path, parser, queries))),
87+
'features': sorted(list(module_features - {'proof'})),
7788
'models': [
7889
{
7990
'path': tla_utils.to_posix(cfg_path),
@@ -83,8 +94,8 @@ def generate_new_manifest(examples_root, spec_path, spec_name, parser, queries):
8394
}
8495
for cfg_path in sorted(get_cfg_files(examples_root, tla_path))
8596
]
86-
}
87-
for tla_path in sorted(get_tla_files(examples_root, spec_path))
97+
} | ({'proof' : {'runtime': 'unknown'}} if 'proof' in module_features else {})
98+
for tla_path, module_features in get_tla_file_features(examples_root, spec_path, parser, queries)
8899
]
89100
}
90101

@@ -107,9 +118,13 @@ def find_corresponding_module(old_module, new_spec):
107118
return modules[0] if any(modules) else None
108119

109120
def integrate_module_info(old_module, new_module):
110-
fields = []
111-
for field in fields:
121+
required_fields = []
122+
for field in required_fields:
112123
new_module[field] = old_module[field]
124+
optional_fields = ['proof']
125+
for field in optional_fields:
126+
if field in old_module:
127+
new_module[field] = old_module[field]
113128

114129
def find_corresponding_model(old_model, new_module):
115130
models = [

.github/scripts/tla_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ def parse_timespan(unparsed):
108108
pattern = '%H:%M:%S'
109109
return timedelta.max if unparsed == 'unknown' else datetime.strptime(unparsed, pattern) - datetime.strptime('00:00:00', pattern)
110110

111+
def format_timespan(t):
112+
"""
113+
Formats the given timedelta into a HH:MM:SS string. If a timespan of
114+
nonzero length, rounds up to minimum of one second.
115+
"""
116+
if t < timedelta(seconds = 1):
117+
return '00:00:01' if t > timedelta.min else '00:00:00'
118+
else:
119+
seconds = int(t.total_seconds())
120+
return f'{seconds // 3600:02d}:{seconds % 3600 // 60:02d}:{seconds % 3600 % 60:02d}'
121+
111122
def get_run_mode(mode):
112123
"""
113124
Converts the model run mode found in manifest.json into TLC CLI

.github/workflows/CI.yml

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,23 +136,9 @@ jobs:
136136
- name: Check proofs
137137
if: matrix.os != 'windows-latest' && !matrix.unicode
138138
run: |
139-
SKIP=(
140-
# Long-running; see https://github.com/tlaplus/tlapm/issues/85
141-
specifications/ewd998/EWD998_proof.tla
142-
specifications/Bakery-Boulangerie/Bakery.tla
143-
specifications/Bakery-Boulangerie/Boulanger.tla
144-
specifications/LoopInvariance/Quicksort.tla
145-
specifications/LoopInvariance/SumSequence.tla
146-
specifications/lamport_mutex/LamportMutex_proofs.tla
147-
specifications/bcastByz/bcastByz.tla
148-
# specifications/MisraReachability/ReachabilityProofs.tla
149-
specifications/byzpaxos/VoteProof.tla
150-
specifications/byzpaxos/BPConProof.tla # Takes about 30 minutes
151-
)
152139
python $SCRIPT_DIR/check_proofs.py \
153140
--tlapm_path $DEPS_DIR/tlapm \
154-
--examples_root . \
155-
--skip "${SKIP[@]}"
141+
--examples_root .
156142
- name: Smoke-test manifest generation script
157143
run: |
158144
python $SCRIPT_DIR/generate_manifest.py \

CONTRIBUTING.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Steps:
4545
- Spec authors: a list of people who authored the spec
4646
- Spec tags:
4747
- `"beginner"` if your spec is appropriate for TLA⁺ newcomers
48+
- Module proof runtime: if module contains formal proofs, record the approximate time necessary to check the proofs with TLAPM on an ordinary workstation; add `"proof" : { "runtime" : "HH:MM:SS" }` to the module fields at the same level as the `communityDependencies` and `models`
49+
- If less than one minute, proof will be checked in its entirety by the CI
4850
- Model runtime: approximate model runtime on an ordinary workstation, in `"HH:MM:SS"` format
4951
- If less than 30 seconds, will be run in its entirety by the CI; otherwise will only be smoke-tested for 5 seconds
5052
- Model mode:
@@ -68,7 +70,7 @@ Steps:
6870
- A checkmark indicating whether the spec is appropriate for beginners
6971
- Checked IFF (if and only if) `beginner` is present in the `tags` field of your spec in `manifest.json`
7072
- A checkmark indicating whether the spec contains a formal proof
71-
- Checked IFF a `proof` tag is present in the `features` field of least one module under your spec in `manifest.json`
73+
- Checked IFF `proof` runtime details are present in at least one module under your spec in `manifest.json`
7274
- A checkmark indicating whether the spec contains PlusCal
7375
- Checked IFF a `pluscal` tag is present in the `features` field of least one module under your spec in `manifest.json`
7476
- A checkmark indicating whether the spec contains a TLC-checkable model

manifest-schema.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,25 @@
2929
},
3030
"features": {
3131
"type": "array",
32-
"items": {"enum": ["pluscal", "proof", "action composition"]}
32+
"items": {"enum": ["pluscal", "action composition"]}
33+
},
34+
"proof": {
35+
"type": "object",
36+
"required": ["runtime"],
37+
"additionalProperties": false,
38+
"properties": {
39+
"runtime": {
40+
"type": "string",
41+
"pattern": "^(([0-9][0-9]:[0-9][0-9]:[0-9][0-9])|unknown)$"
42+
}
43+
}
3344
},
3445
"models": {
3546
"type": "array",
3647
"items": {
3748
"type": "object",
38-
"additionalProperties": false,
3949
"required": ["path", "runtime", "mode", "result"],
50+
"additionalProperties": false,
4051
"properties": {
4152
"path": {"type": "string"},
4253
"runtime": {
@@ -53,13 +64,13 @@
5364
},
5465
{
5566
"type": "object",
56-
"additionalProperties": false,
5767
"required": ["simulate"],
68+
"additionalProperties": false,
5869
"properties": {
5970
"simulate": {
6071
"type": "object",
61-
"additionalProperties": false,
6272
"required": ["traceCount"],
73+
"additionalProperties": false,
6374
"properties": {
6475
"traceCount": {"type": "number"}
6576
}

specifications/Bakery-Boulangerie/manifest.json

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,23 @@
1010
"path": "specifications/Bakery-Boulangerie/Bakery.tla",
1111
"communityDependencies": [],
1212
"features": [
13-
"pluscal",
14-
"proof"
13+
"pluscal"
1514
],
16-
"models": []
15+
"models": [],
16+
"proof": {
17+
"runtime": "00:00:01"
18+
}
1719
},
1820
{
1921
"path": "specifications/Bakery-Boulangerie/Boulanger.tla",
2022
"communityDependencies": [],
2123
"features": [
22-
"pluscal",
23-
"proof"
24+
"pluscal"
2425
],
25-
"models": []
26+
"models": [],
27+
"proof": {
28+
"runtime": "00:00:01"
29+
}
2630
},
2731
{
2832
"path": "specifications/Bakery-Boulangerie/MCBakery.tla",

specifications/FiniteMonotonic/manifest.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
{
2121
"path": "specifications/FiniteMonotonic/CRDT_proof.tla",
2222
"communityDependencies": [],
23-
"features": [
24-
"proof"
25-
],
26-
"models": []
23+
"features": [],
24+
"models": [],
25+
"proof": {
26+
"runtime": "00:00:01"
27+
}
2728
},
2829
{
2930
"path": "specifications/FiniteMonotonic/DistributedReplicatedLog.tla",

0 commit comments

Comments
 (0)