2024-04-19 10:17:58 +03:00
#!/usr/bin/python3
# SPDX-License-Identifier: LGPL-2.1-or-later
''' Test wrapper command for driving integration tests.
'''
import argparse
2024-05-07 13:24:51 +03:00
import json
2024-04-19 10:17:58 +03:00
import os
import shlex
import subprocess
import sys
import textwrap
from pathlib import Path
EMERGENCY_EXIT_DROPIN = """ \
[ Unit ]
Wants = emergency - exit . service
"""
EMERGENCY_EXIT_SERVICE = """ \
[ Unit ]
DefaultDependencies = no
Conflicts = shutdown . target
Conflicts = rescue . service
Before = shutdown . target
Before = rescue . service
FailureAction = exit
[ Service ]
ExecStart = false
"""
def main ( ) :
parser = argparse . ArgumentParser ( description = __doc__ )
2024-05-27 12:15:02 +03:00
parser . add_argument ( ' --mkosi ' , required = True )
2024-04-19 10:17:58 +03:00
parser . add_argument ( ' --meson-source-dir ' , required = True , type = Path )
parser . add_argument ( ' --meson-build-dir ' , required = True , type = Path )
2024-05-11 20:40:03 +03:00
parser . add_argument ( ' --name ' , required = True )
2024-05-12 11:50:47 +03:00
parser . add_argument ( ' --unit ' , required = True )
2024-04-30 20:06:00 +03:00
parser . add_argument ( ' --storage ' , required = True )
2024-05-05 19:14:44 +03:00
parser . add_argument ( ' --firmware ' , required = True )
2024-05-07 16:20:44 +03:00
parser . add_argument ( ' --slow ' , action = argparse . BooleanOptionalAction )
2024-05-28 16:54:35 +03:00
parser . add_argument ( ' --vm ' , action = argparse . BooleanOptionalAction )
2024-05-15 11:09:53 +03:00
parser . add_argument ( ' --exit-code ' , required = True , type = int )
2024-04-19 10:17:58 +03:00
parser . add_argument ( ' mkosi_args ' , nargs = " * " )
args = parser . parse_args ( )
2024-05-07 16:20:44 +03:00
if not bool ( int ( os . getenv ( " SYSTEMD_INTEGRATION_TESTS " , " 0 " ) ) ) :
2024-05-11 20:40:03 +03:00
print ( f " SYSTEMD_INTEGRATION_TESTS=1 not found in environment, skipping { args . name } " , file = sys . stderr )
2024-05-07 16:20:44 +03:00
exit ( 77 )
if args . slow and not bool ( int ( os . getenv ( " SYSTEMD_SLOW_TESTS " , " 0 " ) ) ) :
2024-05-11 20:40:03 +03:00
print ( f " SYSTEMD_SLOW_TESTS=1 not found in environment, skipping { args . name } " , file = sys . stderr )
2024-05-07 16:20:44 +03:00
exit ( 77 )
2024-06-17 16:09:40 +03:00
if args . vm and bool ( int ( os . getenv ( " TEST_NO_QEMU " , " 0 " ) ) ) :
print ( f " TEST_NO_QEMU=1, skipping { args . name } " , file = sys . stderr )
exit ( 77 )
2024-08-15 00:49:10 +03:00
if args . name in os . getenv ( " TEST_SKIP " , " " ) . split ( ) :
print ( f " Skipping { args . name } due to TEST_SKIP " , file = sys . stderr )
exit ( 77 )
2024-06-24 17:20:11 +03:00
keep_journal = os . getenv ( " TEST_SAVE_JOURNAL " , " fail " )
2024-08-02 16:46:41 +03:00
shell = bool ( int ( os . getenv ( " TEST_SHELL " , " 0 " ) ) )
if shell and not sys . stderr . isatty ( ) :
print ( f " --interactive must be passed to meson test to use TEST_SHELL=1 " , file = sys . stderr )
exit ( 1 )
2024-06-24 17:20:11 +03:00
2024-05-11 20:40:03 +03:00
name = args . name + ( f " - { i } " if ( i := os . getenv ( " MESON_TEST_ITERATION " ) ) else " " )
2024-04-19 10:17:58 +03:00
dropin = textwrap . dedent (
""" \
[ Service ]
StandardOutput = journal + console
"""
)
2024-08-02 16:46:41 +03:00
if not shell :
dropin + = textwrap . dedent (
f """
[ Unit ]
SuccessAction = exit
SuccessActionExitStatus = 123
"""
)
2024-05-03 11:27:58 +03:00
if os . getenv ( " TEST_MATCH_SUBTEST " ) :
dropin + = textwrap . dedent (
f """
[ Service ]
Environment = TEST_MATCH_SUBTEST = { os . environ [ " TEST_MATCH_SUBTEST " ] }
"""
)
if os . getenv ( " TEST_MATCH_TESTCASE " ) :
dropin + = textwrap . dedent (
f """
[ Service ]
Environment = TEST_MATCH_TESTCASE = { os . environ [ " TEST_MATCH_TESTCASE " ] }
"""
)
2024-08-02 16:46:41 +03:00
journal_file = None
2024-04-19 10:17:58 +03:00
if not sys . stderr . isatty ( ) :
dropin + = textwrap . dedent (
"""
[ Unit ]
FailureAction = exit
"""
)
2024-05-03 11:57:50 +03:00
journal_file = ( args . meson_build_dir / ( f " test/journal/ { name } .journal " ) ) . absolute ( )
2024-04-19 10:17:58 +03:00
journal_file . unlink ( missing_ok = True )
2024-08-02 16:46:41 +03:00
elif not shell :
2024-06-28 14:34:37 +03:00
dropin + = textwrap . dedent (
"""
[ Unit ]
Wants = multi - user . target
"""
)
2024-04-19 10:17:58 +03:00
cmd = [
2024-05-27 12:15:02 +03:00
args . mkosi ,
2024-04-19 10:17:58 +03:00
' --directory ' , os . fspath ( args . meson_source_dir ) ,
' --output-dir ' , os . fspath ( args . meson_build_dir / ' mkosi.output ' ) ,
' --extra-search-path ' , os . fspath ( args . meson_build_dir ) ,
2024-05-03 11:57:50 +03:00
' --machine ' , name ,
2024-04-19 10:17:58 +03:00
' --ephemeral ' ,
* ( [ ' --forward-journal ' , journal_file ] if journal_file else [ ] ) ,
* (
[
' --credential ' ,
f " systemd.extra-unit.emergency-exit.service= { shlex . quote ( EMERGENCY_EXIT_SERVICE ) } " ,
' --credential ' ,
f " systemd.unit-dropin.emergency.target= { shlex . quote ( EMERGENCY_EXIT_DROPIN ) } " ,
]
if not sys . stderr . isatty ( )
else [ ]
) ,
' --credential ' ,
2024-05-12 11:50:47 +03:00
f " systemd.unit-dropin. { args . unit } = { shlex . quote ( dropin ) } " ,
2024-04-25 12:00:15 +03:00
' --runtime-network=none ' ,
2024-04-28 21:46:14 +03:00
' --runtime-scratch=no ' ,
2024-05-12 16:16:37 +03:00
* args . mkosi_args ,
2024-05-05 19:14:44 +03:00
' --qemu-firmware ' , args . firmware ,
2024-08-20 15:39:45 +03:00
* ( [ ' --qemu-kvm ' , ' no ' ] if int ( os . getenv ( " TEST_NO_KVM " , " 0 " ) ) else [ ] ) ,
2024-04-19 10:17:58 +03:00
' --kernel-command-line-extra ' ,
' ' . join ( [
' systemd.hostname=H ' ,
2024-05-11 20:40:03 +03:00
f " SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/ { args . name } .units:/usr/lib/systemd/tests/testdata/units: " ,
2024-08-02 16:46:41 +03:00
* ( [ f " systemd.unit= { args . unit } " ] if not shell else [ ] ) ,
2024-04-28 23:51:41 +03:00
' systemd.mask=systemd-networkd-wait-online.service ' ,
2024-04-28 20:28:37 +03:00
* (
[
" systemd.mask=serial-getty@.service " ,
" systemd.show_status=no " ,
" systemd.crash_shell=0 " ,
2024-04-29 11:47:25 +03:00
" systemd.crash_action=poweroff " ,
2024-04-28 20:28:37 +03:00
]
if not sys . stderr . isatty ( )
else [ ]
) ,
2024-04-19 10:17:58 +03:00
] ) ,
2024-05-02 09:52:50 +03:00
' --credential ' , f " journal.storage= { ' persistent ' if sys . stderr . isatty ( ) else args . storage } " ,
2024-07-29 14:40:42 +03:00
* ( [ ' --runtime-build-sources=no ' ] if not sys . stderr . isatty ( ) else [ ] ) ,
2024-05-28 16:54:35 +03:00
' qemu ' if args . vm or os . getuid ( ) != 0 else ' boot ' ,
2024-04-19 10:17:58 +03:00
]
2024-04-23 16:13:22 +03:00
result = subprocess . run ( cmd )
2024-05-07 12:50:11 +03:00
2024-10-07 18:48:55 +03:00
# On Debian/Ubuntu we get a lot of random QEMU crashes. Retry once, and then skip if it fails again.
if args . vm and result . returncode == 247 and args . exit_code != 247 :
journal_file . unlink ( missing_ok = True )
result = subprocess . run ( cmd )
if args . vm and result . returncode == 247 and args . exit_code != 247 :
print ( f " Test { args . name } failed due to QEMU crash (error 247), ignoring " , file = sys . stderr )
exit ( 77 )
2024-06-24 17:20:11 +03:00
if journal_file and ( keep_journal == " 0 " or ( result . returncode in ( args . exit_code , 77 ) and keep_journal == " fail " ) ) :
journal_file . unlink ( missing_ok = True )
2024-05-07 12:50:11 +03:00
2024-08-02 16:46:41 +03:00
if shell or result . returncode in ( args . exit_code , 77 ) :
exit ( 0 if shell or result . returncode == args . exit_code else 77 )
2024-04-19 10:17:58 +03:00
if journal_file :
2024-05-07 13:24:51 +03:00
ops = [ ]
if os . getenv ( " GITHUB_ACTIONS " ) :
id = os . environ [ " GITHUB_RUN_ID " ]
iteration = os . environ [ " GITHUB_RUN_ATTEMPT " ]
j = json . loads (
subprocess . run (
[
2024-05-27 12:15:02 +03:00
args . mkosi ,
2024-05-07 13:24:51 +03:00
" --directory " , os . fspath ( args . meson_source_dir ) ,
" --json " ,
" summary " ,
] ,
stdout = subprocess . PIPE ,
text = True ,
) . stdout
)
2024-07-08 13:59:52 +03:00
distribution = j [ " Images " ] [ - 1 ] [ " Distribution " ]
release = j [ " Images " ] [ - 1 ] [ " Release " ]
2024-05-07 13:24:51 +03:00
artifact = f " ci-mkosi- { id } - { iteration } - { distribution } - { release } -failed-test-journals "
ops + = [ f " gh run download { id } --name { artifact } -D ci/ { artifact } " ]
journal_file = Path ( f " ci/ { artifact } /test/journal/ { name } .journal " )
2024-05-12 11:50:47 +03:00
ops + = [ f " journalctl --file { journal_file } --no-hostname -o short-monotonic -u { args . unit } -p info " ]
2024-05-07 13:24:51 +03:00
2024-05-07 12:50:11 +03:00
print ( " Test failed, relevant logs can be viewed with: \n \n "
2024-05-07 13:24:51 +03:00
f " { ( ' && ' . join ( ops ) ) } \n " , file = sys . stderr )
2024-05-07 12:50:11 +03:00
# 0 also means we failed so translate that to a non-zero exit code to mark the test as failed.
exit ( result . returncode or 1 )
2024-04-19 10:17:58 +03:00
if __name__ == ' __main__ ' :
main ( )