Skip to content

Commit 7731bc3

Browse files
committed
feat: [OCISDEV-793] reduce ci duration, parallelize jobs inside single runner
1 parent b495d79 commit 7731bc3

4 files changed

Lines changed: 182 additions & 81 deletions

File tree

.github/workflows/acceptance-tests.yml

Lines changed: 105 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -105,48 +105,30 @@ jobs:
105105
fail-fast: false
106106
matrix:
107107
suite:
108-
# contract & locks
109-
- apiContract
110-
- apiLocks
111-
# settings & notifications (needs email)
112-
- apiSettings
113-
- apiNotification
114-
- apiCors
115-
# graph
116-
- apiGraphUser
117-
- apiGraph
118-
- apiGraphGroup
119-
# spaces & dav
120-
- apiSpaces
121-
- apiSpacesShares
122-
- apiSpacesDavOperation
123-
- apiDownloads
124-
- apiAsyncUpload
125-
- apiDepthInfinity
126-
- apiArchiver
127-
- apiActivities
128-
# search
129-
- apiSearch1
130-
- apiSearch2
131-
- apiSearchContent # needs Tika
132-
# sharing
133-
- apiSharingNgShares
134-
- apiReshare
135-
- apiSharingNgPermissions
136-
- apiSharingNgAdditionalShareRole
137-
- apiSharingNgDriveInvitation
138-
- apiSharingNgItemInvitation
139-
- apiSharingNgDriveLinkShare
140-
- apiSharingNgItemLinkShare
141-
- apiSharingNgLinkShareManagement
142-
# auth
143-
- apiAuthApp
144-
# antivirus (needs ClamAV)
145-
- apiAntivirus
146-
# federation (needs email + federation ocis)
147-
- apiOcm
148-
# collaboration (needs WOPI)
149-
- apiCollaboration
108+
# contract & locks — 2 suites in parallel (contractAndLock group)
109+
- "apiContract,apiLocks"
110+
# settings, notifications & cors — 3 suites in parallel (needs email)
111+
- "apiSettings,apiNotification,apiCors"
112+
# graph + search-no-tika — 4 suites in parallel (graphUser + groupAndSearch1 groups)
113+
- "apiGraphUser,apiSearch1,apiGraph,apiGraphGroup"
114+
# spaces — 2 suites in parallel (spaces + spacesShares groups)
115+
- "apiSpaces,apiSpacesShares"
116+
# dav operations — 6 suites in parallel (davOperations group)
117+
- "apiSpacesDavOperation,apiDownloads,apiAsyncUpload,apiDepthInfinity,apiArchiver,apiActivities"
118+
# search with Tika — 2 suites in parallel (search2 group)
119+
- "apiSearch2,apiSearchContent"
120+
# sharing ng part 1 — 4 suites in parallel (sharingNg1 + sharingNgAdditionalShareRole groups)
121+
- "apiSharingNgShares,apiReshare,apiSharingNgPermissions,apiSharingNgAdditionalShareRole"
122+
# sharing ng part 2 — 5 suites in parallel (sharingNgShareInvitation + sharingNgLinkShare groups)
123+
- "apiSharingNgDriveInvitation,apiSharingNgItemInvitation,apiSharingNgDriveLinkShare,apiSharingNgItemLinkShare,apiSharingNgLinkShareManagement"
124+
# auth app — kept isolated (enables auth-app service + PROXY_ENABLE_APP_AUTH)
125+
- "apiAuthApp"
126+
# antivirus — kept isolated (needs ClamAV)
127+
- "apiAntivirus"
128+
# federation — kept isolated (needs email + federation ocis)
129+
- "apiOcm"
130+
# collaboration — kept isolated (needs WOPI)
131+
- "apiCollaboration"
150132
steps:
151133
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
152134
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
@@ -216,7 +198,16 @@ jobs:
216198
composer --version
217199
218200
- name: Run ${{ matrix.suite }}
219-
run: BEHAT_SUITES=${{ matrix.suite }} python3 tests/acceptance/run-github.py
201+
run: |
202+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
203+
BEHAT_SUITES="${{ matrix.suite }}" python3 tests/acceptance/run-github.py
204+
EXIT=$?
205+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
206+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
207+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
208+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
209+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
210+
exit $EXIT
220211
221212
cli-tests:
222213
needs: [build-and-test]
@@ -252,7 +243,16 @@ jobs:
252243
composer --version
253244
254245
- name: Run ${{ matrix.suite }}
255-
run: BEHAT_SUITES="${{ matrix.suite }}" python3 tests/acceptance/run-github.py
246+
run: |
247+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
248+
BEHAT_SUITES="${{ matrix.suite }}" python3 tests/acceptance/run-github.py
249+
EXIT=$?
250+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
251+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
252+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
253+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
254+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
255+
exit $EXIT
256256
257257
core-api-tests:
258258
name: ${{ matrix.suite }}
@@ -338,11 +338,17 @@ jobs:
338338
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
339339
340340
- name: Run ${{ matrix.suite }}
341-
run: >
342-
BEHAT_SUITES="${{ matrix.suite }}"
343-
ACCEPTANCE_TEST_TYPE=core-api
344-
WITH_REMOTE_PHP=true
345-
python3 tests/acceptance/run-github.py
341+
run: |
342+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
343+
BEHAT_SUITES="${{ matrix.suite }}" ACCEPTANCE_TEST_TYPE=core-api WITH_REMOTE_PHP=true \
344+
python3 tests/acceptance/run-github.py
345+
EXIT=$?
346+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
347+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
348+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
349+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
350+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
351+
exit $EXIT
346352
347353
e2e-tests:
348354
name: e2e-${{ matrix.suite }}
@@ -449,7 +455,16 @@ jobs:
449455
echo "keycloak ready."
450456
451457
- name: Run e2e-${{ matrix.suite }}
452-
run: E2E_ARGS="${{ matrix.args }}" python3 tests/acceptance/run-e2e.py
458+
run: |
459+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
460+
E2E_ARGS="${{ matrix.args }}" python3 tests/acceptance/run-e2e.py
461+
EXIT=$?
462+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
463+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
464+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
465+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
466+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
467+
exit $EXIT
453468
env:
454469
TIKA_NEEDED: ${{ matrix.tika == true && 'true' || 'false' }}
455470
KEYCLOAK_NEEDED: ${{ matrix.keycloak == true && 'true' || 'false' }}
@@ -465,7 +480,16 @@ jobs:
465480
go-version-file: go.mod
466481
cache: true
467482
- name: Run litmus
468-
run: python3 tests/acceptance/run-litmus.py
483+
run: |
484+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
485+
python3 tests/acceptance/run-litmus.py
486+
EXIT=$?
487+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
488+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
489+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
490+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
491+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
492+
exit $EXIT
469493
470494
cs3api:
471495
name: cs3api
@@ -478,7 +502,16 @@ jobs:
478502
go-version-file: go.mod
479503
cache: true
480504
- name: Run cs3api validator
481-
run: python3 tests/acceptance/run-cs3api.py
505+
run: |
506+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
507+
python3 tests/acceptance/run-cs3api.py
508+
EXIT=$?
509+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
510+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
511+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
512+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
513+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
514+
exit $EXIT
482515
483516
wopi-builtin:
484517
name: wopi-builtin
@@ -491,7 +524,16 @@ jobs:
491524
go-version-file: go.mod
492525
cache: true
493526
- name: Run WOPI validator (builtin)
494-
run: python3 tests/acceptance/run-wopi.py --type builtin
527+
run: |
528+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
529+
python3 tests/acceptance/run-wopi.py --type builtin
530+
EXIT=$?
531+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
532+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
533+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
534+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
535+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
536+
exit $EXIT
495537
496538
wopi-cs3:
497539
name: wopi-cs3
@@ -504,7 +546,16 @@ jobs:
504546
go-version-file: go.mod
505547
cache: true
506548
- name: Run WOPI validator (cs3)
507-
run: python3 tests/acceptance/run-wopi.py --type cs3
549+
run: |
550+
vmstat 2 > /tmp/vmstat.log & MONITOR_PID=$!
551+
python3 tests/acceptance/run-wopi.py --type cs3
552+
EXIT=$?
553+
kill $MONITOR_PID 2>/dev/null; wait $MONITOR_PID 2>/dev/null || true
554+
awk '/^[ ]*[0-9]/ { busy=100-$15; sum_b+=busy; if(busy>pk_b)pk_b=busy;
555+
sum_r+=$1; if($1>pk_r) pk_r=$1; sum_wa+=$16; n++ }
556+
END { printf "=== load (2 vCPU): avg busy %d%% peak %d%% | avg runq %.0f peak %d | avg wa %d%%\n",
557+
sum_b/n, pk_b, sum_r/n, pk_r, sum_wa/n }' /tmp/vmstat.log
558+
exit $EXIT
508559
509560
all-acceptance-tests:
510561
needs: [local-api-tests, cli-tests, core-api-tests, litmus, cs3api, wopi-builtin, wopi-cs3, e2e-tests]

tests/acceptance/run-github.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -694,12 +694,42 @@ def cleanup(*_):
694694
}
695695
behat_env.update(cfg["extraEnvironment"])
696696

697-
print(f"Running suites: {behat_suites_raw} (type: {acceptance_test_type})")
698-
result = subprocess.run(
699-
["make", "-C", str(repo_root), "test-acceptance-api"],
700-
env=behat_env,
701-
)
702-
return result.returncode
697+
print(f"Running suites: {suites} (type: {acceptance_test_type})")
698+
699+
if len(suites) == 1:
700+
# single suite: stream output directly
701+
result = subprocess.run(
702+
["make", "-C", str(repo_root), "test-acceptance-api"],
703+
env=behat_env,
704+
)
705+
return result.returncode
706+
707+
# multiple suites: run in parallel against the shared OCIS instance,
708+
# one behat process per suite, output captured and printed serially on completion.
709+
import concurrent.futures
710+
711+
def run_suite(suite: str) -> tuple:
712+
env = {**behat_env, "BEHAT_SUITES": suite}
713+
proc = subprocess.run(
714+
["make", "-C", str(repo_root), "test-acceptance-api"],
715+
env=env, capture_output=True, text=True,
716+
)
717+
return suite, proc.returncode, proc.stdout + proc.stderr
718+
719+
results = []
720+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(suites)) as pool:
721+
fut_to_suite = {pool.submit(run_suite, s): s for s in suites}
722+
for fut in concurrent.futures.as_completed(fut_to_suite):
723+
suite, rc, output = fut.result()
724+
print(f"\n{'=' * 60}\n=== suite: {suite} (exit {rc}) ===\n{'=' * 60}", flush=True)
725+
print(output, flush=True)
726+
results.append((suite, rc))
727+
728+
failed = [s for s, rc in results if rc != 0]
729+
if failed:
730+
print(f"FAILED suites: {failed}")
731+
return 1
732+
return 0
703733

704734
finally:
705735
cleanup()

tests/acceptance/run-litmus.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ def setup_for_litmus(ocis_url: str) -> tuple:
144144
return space_id, public_token
145145

146146

147-
def run_litmus(name: str, endpoint: str) -> int:
148-
print(f"\nTesting endpoint [{name}]: {endpoint}", flush=True)
147+
def run_litmus(name: str, endpoint: str) -> tuple:
149148
result = subprocess.run(
150149
["docker", "run", "--rm",
151150
"-e", f"LITMUS_URL={endpoint}",
@@ -156,8 +155,9 @@ def run_litmus(name: str, endpoint: str) -> int:
156155
# No extra CMD — ENTRYPOINT is already litmus-wrapper; passing it again
157156
# would make the wrapper use the path as LITMUS_URL, overriding the env var.
158157
],
158+
capture_output=True, text=True,
159159
)
160-
return result.returncode
160+
return result.returncode, result.stdout + result.stderr
161161

162162

163163
def main() -> int:
@@ -224,11 +224,21 @@ def cleanup(*_):
224224
("spaces-endpoint", f"{litmus_base}/remote.php/dav/spaces/{space_id}"),
225225
]
226226

227-
failed = []
228-
for name, endpoint in endpoints:
229-
rc = run_litmus(name, endpoint)
230-
if rc != 0:
231-
failed.append(name)
227+
import concurrent.futures
228+
229+
# all 5 endpoints are independent docker containers — run in parallel
230+
print(f"Running {len(endpoints)} litmus endpoints in parallel", flush=True)
231+
results = []
232+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(endpoints)) as pool:
233+
fut_to_name = {pool.submit(run_litmus, n, e): n for n, e in endpoints}
234+
for fut in concurrent.futures.as_completed(fut_to_name):
235+
name = fut_to_name[fut]
236+
rc, output = fut.result()
237+
print(f"\n{'=' * 60}\n=== endpoint: {name} (exit {rc}) ===\n{'=' * 60}", flush=True)
238+
print(output, flush=True)
239+
results.append((name, rc))
240+
241+
failed = [name for name, rc in results if rc != 0]
232242

233243
if failed:
234244
print(f"\nFailed endpoints: {', '.join(failed)}", file=sys.stderr)

tests/acceptance/run-wopi.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,7 @@ def prepare_test_file(bridge_ip: str) -> tuple:
218218

219219

220220
def run_validator(group: str, token: str, wopi_src: str, ttl: str,
221-
secure: bool = False) -> int:
222-
print(f"\nRunning testgroup [{group}] secure={secure}", flush=True)
221+
secure: bool = False) -> tuple:
223222
cmd = [
224223
"docker", "run", "--rm",
225224
"--workdir", "/app",
@@ -229,7 +228,8 @@ def run_validator(group: str, token: str, wopi_src: str, ttl: str,
229228
if secure:
230229
cmd.append("-s")
231230
cmd += ["-t", token, "-w", wopi_src, "-l", ttl, "--testgroup", group]
232-
return subprocess.run(cmd).returncode
231+
result = subprocess.run(cmd, capture_output=True, text=True)
232+
return result.returncode, result.stdout + result.stderr
233233

234234

235235
def main() -> int:
@@ -348,18 +348,28 @@ def cleanup(*_):
348348
# --- prepare-test-file: upload file, get WOPI credentials ---
349349
access_token, ttl, wopi_src = prepare_test_file(bridge_ip)
350350

351-
# --- Run validator for each testgroup ---
352-
failed = []
353-
for group in SHARED_TESTGROUPS:
354-
rc = run_validator(group, access_token, wopi_src, ttl, secure=False)
355-
if rc != 0:
356-
failed.append(group)
351+
# --- Run all validator testgroups in parallel ---
352+
import concurrent.futures
357353

354+
all_groups = [(g, False) for g in SHARED_TESTGROUPS]
358355
if wopi_type == "builtin":
359-
for group in BUILTIN_ONLY_TESTGROUPS:
360-
rc = run_validator(group, access_token, wopi_src, ttl, secure=True)
361-
if rc != 0:
362-
failed.append(group)
356+
all_groups += [(g, True) for g in BUILTIN_ONLY_TESTGROUPS]
357+
358+
print(f"Running {len(all_groups)} WOPI testgroups in parallel", flush=True)
359+
results = []
360+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(all_groups)) as pool:
361+
fut_to_group = {
362+
pool.submit(run_validator, g, access_token, wopi_src, ttl, s): g
363+
for g, s in all_groups
364+
}
365+
for fut in concurrent.futures.as_completed(fut_to_group):
366+
group = fut_to_group[fut]
367+
rc, output = fut.result()
368+
print(f"\n{'=' * 60}\n=== testgroup: {group} (exit {rc}) ===\n{'=' * 60}", flush=True)
369+
print(output, flush=True)
370+
results.append((group, rc))
371+
372+
failed = [g for g, rc in results if rc != 0]
363373

364374
if failed:
365375
print(f"\nFailed testgroups: {', '.join(failed)}", file=sys.stderr)

0 commit comments

Comments
 (0)