Skip to content

Commit e9664b5

Browse files
authored
[ci] Implement more of CI workflows. Fix bugs and improve features in validation and execution scripts. (#3)
1 parent 32292aa commit e9664b5

7 files changed

Lines changed: 226 additions & 82 deletions

File tree

.github/workflows/build.yml

Lines changed: 0 additions & 37 deletions
This file was deleted.

.github/workflows/build/action.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: build
2+
3+
inputs:
4+
cache-key:
5+
required: true
6+
7+
runs:
8+
using: 'composite'
9+
10+
steps:
11+
- name: configure
12+
shell: bash
13+
run: |
14+
cmake -H'simc-profile/simc/' \
15+
-B'simc-profile/simc/b/ninja' -GNinja -DBUILD_GUI=OFF \
16+
-DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++-18 \
17+
-DCMAKE_CXX_STANDARD=17 -DSC_NO_NETWORKING=ON
18+
19+
- name: build
20+
shell: bash
21+
run: |
22+
ninja -C 'simc-profile/simc/b/ninja'
23+
24+
- uses: actions/cache/save@v5
25+
with:
26+
path: simc-profile/simc/b/ninja/simc
27+
key: simc-clang++-18-cpp17-${{ inputs.cache-key }}

.github/workflows/main.yml

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,53 @@ name: CI
33
on: [pull_request, push]
44

55
jobs:
6-
build:
7-
uses: ./.github/workflows/build.yml
6+
CI:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v6
11+
with:
12+
fetch-depth: 2
13+
14+
- name: modified_files
15+
run: |
16+
files=$(git diff --diff-filter=AM --no-commit-id --name-only HEAD~1 -- '***.simc' | xargs)
17+
echo "MODIFIED_FILES=$files" >> $GITHUB_ENV
18+
19+
- name: validate_python
20+
run: |
21+
python ${{ runner.workspace }}/simc-profile/scripts/validate.py \
22+
${{ env.MODIFIED_FILES }}
23+
24+
- uses: ./.github/workflows/setup_simc
25+
id: setup
26+
27+
- name: validate_simc_pr
28+
if: github.event_name == 'pull_request'
29+
run: |
30+
python ${{ runner.workspace }}/simc-profile/scripts/execute.py \
31+
${{ env.MODIFIED_FILES }} -b ${{ runner.workspace }}/simc-profile/simc-profile/simc/b/ninja/simc \
32+
--save .
33+
34+
- name: execute_simc_pr
35+
if: github.event_name == 'pull_request'
36+
run: |
37+
python ${{ runner.workspace }}/simc-profile/scripts/execute.py \
38+
${{ env.MODIFIED_FILES }} -b ${{ runner.workspace }}/simc-profile/simc-profile/simc/b/ninja/simc \
39+
--execute
40+
41+
- name: validate_simc_main
42+
if: github.event_name == 'push' && ( success() || failure() ) && github.repository == 'simulationcraft/simc-profile' && github.ref_name == github.event.repository.default_branch
43+
run: |
44+
find ${{ runner.workspace }}/simc-profile/profiles -type f \
45+
| xargs python ${{ runner.workspace }}/simc-profile/scripts/execute.py \
46+
-b ${{ runner.workspace }}/simc-profile/simc-profile/simc/b/ninja/simc \
47+
--save .
48+
49+
- name: execute_simc_main
50+
if: github.event_name == 'push' && ( success() || failure() ) && github.repository == 'simulationcraft/simc-profile' && github.ref_name == github.event.repository.default_branch
51+
run: |
52+
find ${{ runner.workspace }}/simc-profile/profiles -type f \
53+
| xargs python ${{ runner.workspace }}/simc-profile/scripts/execute.py \
54+
-b ${{ runner.workspace }}/simc-profile/simc-profile/simc/b/ninja/simc \
55+
--execute
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: setup_simc
2+
3+
outputs:
4+
rebuilt-simc:
5+
value: ${{ !steps.restore_cache.outputs.cache-hit }}
6+
7+
runs:
8+
using: 'composite'
9+
10+
steps:
11+
- uses: actions/checkout@v6
12+
with:
13+
repository: simulationcraft/simc
14+
path: simc-profile/simc/
15+
16+
- name: cache_key
17+
shell: bash
18+
run: |
19+
cd simc-profile/simc/
20+
echo "SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
21+
cd ../..
22+
23+
- uses: actions/cache/restore@v5
24+
id: restore_cache
25+
continue-on-error: true
26+
with:
27+
path: simc-profile/simc/b/ninja/simc
28+
key: simc-clang++-18-cpp17-${{ env.SHA }}
29+
30+
- uses: ./.github/workflows/build
31+
if: steps.restore_cache.outputs.cache-hit != 'true'
32+
with:
33+
cache-key: ${{ env.SHA }}

scripts/execute.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,73 @@
77
def validate_header_option(line):
88
return ParsedOption(line).validate(HEADER_OPTIONS)
99

10+
def handle_header_line(line, deferral_list):
11+
option = ParsedOption(line)
12+
if option.validate(HEADER_OPTIONS):
13+
if option.scope(HEADER_OPTIONS) == 'player':
14+
deferral_list.append(line)
15+
return ''
16+
else:
17+
return line
18+
return ''
19+
1020
def generate_simc_input(profiles: list[Profile]):
1121
for profile in profiles:
22+
profile.validate()
23+
1224
profile.params = []
25+
deferred_options = []
26+
push_deferred_options = False
1327
with open(profile) as handle:
1428
header = True
1529
for line in handle.readlines():
1630
line = line.strip()
1731
if not len(line):
1832
continue
19-
if line[0] == '#':
20-
if header and validate_header_option(line[1:].strip()):
21-
line = line[1:].strip()
22-
else:
23-
line = ''
33+
if line[0] == '#' and header:
34+
line = handle_header_line(line[1:].strip(), deferred_options)
35+
elif line[0] == '#' and not header:
36+
line = ''
2437
else:
38+
option = ParsedOption(line)
39+
if option.validate_class(profile) and option.validate_class_value(profile):
40+
push_deferred_options = True
2541
header = False
2642

2743
if line != '':
2844
profile.params.append(line)
45+
if push_deferred_options:
46+
profile.params += deferred_options
47+
deferred_options = []
2948

3049
def run_sim(binary: Path, profiles: list[str], prefix: list[str], suffix: list[str] = []):
31-
with subprocess.Popen([binary] + prefix + profiles + suffix, stdout=sys.stdout, stderr=sys.stderr) as _:
32-
pass
50+
proc = subprocess.Popen([binary] + prefix + profiles + suffix, stdout=sys.stdout, stderr=sys.stderr)
51+
proc.wait()
52+
return proc.returncode
3353

3454
def print_dps_data(filename: Path):
35-
with subprocess.Popen(['jq', '[.sim.players[] | {name: .name, dps: .collected_data.dps}]', filename]) as _:
36-
pass
55+
proc = subprocess.Popen(['jq', '[.sim.players[] | {name: .name, dps: .collected_data.dps}]', filename])
56+
proc.wait()
57+
return proc.returncode
3758

3859
def save_profiles(binary: Path, profiles: list[Profile], location: Path):
3960
params = []
4061
for profile in profiles:
4162
params += profile.params
4263
params += [f'save={location}/{profile.expected_name()}.simc']
43-
run_sim(binary, params, ['output=/dev/null'])
64+
return run_sim(binary, params, ['output=/dev/null'])
4465

4566
def run_profiles(binary: Path, profiles: list[Profile]):
4667
prefix = [
4768
'output=/dev/null',
4869
'target_error=0.05',
49-
'json=/tmp/out.json'
70+
'json=output.json',
71+
'html=output.html'
5072
]
51-
run_sim(binary, [line for profile in profiles for line in profile.params], prefix)
73+
return run_sim(binary, [line for profile in profiles for line in profile.params], prefix)
5274

5375
parser = ArgumentParser(prog='SimulationCraft Profile Runner')
54-
parser.add_argument('filenames', nargs='+', type=Profile)
76+
parser.add_argument('filenames', nargs='*', type=Profile)
5577
parser.add_argument('-b', '--binary', type=Path, required=True, metavar='PATH')
5678
parser.add_argument('--save', type=Path, default=False, metavar='PATH', help='root directory to save all profiles')
5779
parser.add_argument('--execute', action='store_true', default=False, help='execute profiles')
@@ -60,9 +82,16 @@ def run_profiles(binary: Path, profiles: list[Profile]):
6082

6183
generate_simc_input(args.filenames)
6284

85+
if not len(args.filenames):
86+
exit(0)
87+
88+
rc = []
6389
if args.save:
64-
save_profiles(args.binary, args.filenames, args.save)
90+
rc.append(save_profiles(args.binary, args.filenames, args.save))
6591

6692
if args.execute:
67-
run_profiles(args.binary, args.filenames)
68-
print_dps_data('/tmp/out.json')
93+
rc.append(run_profiles(args.binary, args.filenames))
94+
rc.append(print_dps_data('output.json'))
95+
96+
print(rc)
97+
exit(max(rc))

scripts/shared.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,27 @@ class Option:
2424
ignore_value: bool
2525
values: list[str]
2626
case_sensitive: bool
27+
scope: str
2728

28-
def __init__(self, key, values=[], ignore_value=False, case_sensitive=True):
29+
def __init__(self, key, values=[], ignore_value=False, case_sensitive=True, scope='player'):
2930
self.key = key
3031
self.values = values
3132
self.ignore_value = ignore_value
3233
self.case_sensitive = case_sensitive
34+
self.scope = scope
3335

3436
def __eq__(self, other: ParsedOption):
35-
if self.key != other.key:
36-
return False
37-
if self.ignore_value:
38-
return True
39-
if self.case_sensitive:
40-
return other.value in self.values
41-
else:
42-
return other.value.lower() in self.values
37+
if isinstance(other, ParsedOption):
38+
if self.key != other.key:
39+
return False
40+
if self.ignore_value:
41+
return True
42+
if self.case_sensitive:
43+
return other.value in self.values
44+
else:
45+
return other.value.lower() in self.values
46+
assert False
47+
return False
4348

4449
class Options:
4550
options: list[Option]
@@ -52,6 +57,10 @@ def __init__(self, *options):
5257
def __contains__(self, other):
5358
return other in self.options
5459

60+
def __iter__(self):
61+
for option in self.options:
62+
yield option
63+
5564
# class (handled separately as value depends on filename)
5665
SIMC_OPTIONS = Options(
5766
Option('level', ['90']),
@@ -96,14 +105,14 @@ def __contains__(self, other):
96105
Option('warlock.default_pet', ['sayaad', 'succubus', 'incubus', 'felguard']),
97106
)
98107
HEADER_OPTIONS = Options(
99-
Option('desired_targets', ignore_value=True),
100-
Option('fight_style', ['patchwerk', 'castingpatchwerk', 'dungeonslice']),
101-
Option('source', ['default']),
102-
Option('potion', ignore_value=True),
103-
Option('flask', ignore_value=True),
104-
Option('food', ignore_value=True),
105-
Option('augmentation', ignore_value=True),
106-
Option('temporary_enchant', ignore_value=True),
108+
Option('desired_targets', ignore_value=True, scope='sim'),
109+
Option('fight_style', ['patchwerk', 'castingpatchwerk', 'dungeonslice'], scope='sim'),
110+
Option('source', ['default'], scope='player'),
111+
Option('potion', ignore_value=True, scope='player'),
112+
Option('flask', ignore_value=True, scope='player'),
113+
Option('food', ignore_value=True, scope='player'),
114+
Option('augmentation', ignore_value=True, scope='player'),
115+
Option('temporary_enchant', ignore_value=True, scope='player'),
107116
)
108117

109118
class Profile:
@@ -125,21 +134,26 @@ def validate(self):
125134
# <class_name>=<class_name>_<spec_name><unnamed>
126135
if not self.path.exists():
127136
print(f'Path {self} does not exist.')
128-
return
137+
return False
129138

130139
class_name, trailing_fragment, spec_name = self.path_parts()
140+
if not class_name and not trailing_fragment and not spec_name:
141+
return False
142+
131143
if class_name not in SPEC_NAMES.keys():
132144
print(f'Profile {self} is not in a `profiles/<class>/` directory.')
133-
return
145+
return False
134146

135147
if spec_name not in SPEC_NAMES[class_name]:
136-
print(f'Profile {self} does not contain a valid specialization name. Try one of {", ".join(SPEC_NAMES[class_name])}.')
137-
return
148+
print(f'Profile {self} does not contain a valid specialization name. It should include one of {", ".join(SPEC_NAMES[class_name])}.')
149+
return False
138150

139151
# python has no way to nicely test if a string contains only printable ascii characters :)
140152
if not all((c in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' for c in trailing_fragment[len(spec_name):])):
141153
print(f'Profile {self} trailing fragment {trailing_fragment[len(spec_name):]} is not alphanumeric.')
142-
return
154+
return False
155+
156+
return True
143157

144158
def expected_name(self):
145159
class_name, trailing_fragment, _ = self.path_parts()
@@ -149,7 +163,7 @@ def path_parts(self):
149163
path_parts = PurePath.relative_to(self.path.resolve(), Path(__file__).resolve(), walk_up=True).parts[2:]
150164
if path_parts[0] != 'profiles':
151165
print(f'Profile {self} is not in the `profiles/` directory.')
152-
return
166+
return False, False, False
153167

154168
trailing_fragment = path_parts[2].split('.')[:-1][0]
155169
return path_parts[1], trailing_fragment, trailing_fragment.split('_')[0]
@@ -192,6 +206,9 @@ def __str__(self):
192206
return f'Invalid Option {self.key}'
193207
return f'{self.key}{self.operator}{self.value}'
194208

209+
def scope(self, options: Options):
210+
return next((o for o in options if o == self)).scope
211+
195212
def validate_class(self, profile: Profile):
196213
class_name, _, _ = profile.path_parts()
197214
return self.parsed and self.key == class_name

0 commit comments

Comments
 (0)