Files
jobtrackingapp/scripts/mariadb-integration.py
T

91 lines
4.5 KiB
Python

"""Run the explicit MariaDB persistence lane, never a SQLite fallback."""
import argparse
import datetime
import json
import os
from pathlib import Path
import re
import shutil
import socket
import subprocess
import uuid
ROOT = Path(__file__).resolve().parent.parent
def compose_command(project):
if not re.fullmatch(r'jobtracker-it-[a-z0-9]+', project):
raise ValueError('Only generated integration projects may be managed.')
return ['docker', 'compose', '--project-name', project, '--project-directory', str(ROOT),
'-f', str(ROOT / 'docker-compose.database.yml'), '--profile', 'integration']
def test_connection(port):
if not 0 < port < 65536: raise ValueError('Invalid integration port.')
return f'Server=127.0.0.1;Port={port};User ID=root;Password=jt-integration-only;SslMode=None'
def existing_connection(environment):
value = environment.get('JOBTRACKER_MARIADB_TEST_ADMIN', '')
if not value.strip(): raise ValueError('--existing-server requires explicit JOBTRACKER_MARIADB_TEST_ADMIN.')
# The fixture independently enforces loopback TCP, exact server version and generated schemas.
return value
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--existing-server', action='store_true', help='Use an explicitly configured isolated loopback server; fixture guards still apply.')
parser.add_argument('--output', default=str(ROOT / '.artifacts/mariadb/integration'))
args = parser.parse_args()
environment = os.environ.copy()
output = Path(args.output).resolve()
output.mkdir(parents=True, exist_ok=True)
project = 'jobtracker-it-' + uuid.uuid4().hex
compose = compose_command(project)
managed = not args.existing_server
started = datetime.datetime.now(datetime.timezone.utc).isoformat()
commands = []
code = 1
def run(command, name):
commands.append({'command': command, 'log': str(output / name)})
with (output / name).open('w', encoding='utf-8') as log:
result = subprocess.run(command, cwd=ROOT, env=environment, stdout=log, stderr=subprocess.STDOUT)
commands[-1]['exitCode'] = result.returncode
return result.returncode
try:
if managed:
with socket.socket() as probe:
probe.bind(('127.0.0.1', 0))
port = probe.getsockname()[1]
environment['MARIADB_TEST_PORT'] = str(port)
environment['JOBTRACKER_MARIADB_TEST_ADMIN'] = test_connection(port)
code = run(compose + ['up', '-d', '--wait', '--wait-timeout', '120', 'mariadb-test'], 'compose-start.log')
else:
existing_connection(environment)
code = 0
if code == 0:
dotnet = environment.get('DOTNET_HOST_PATH') or shutil.which('dotnet')
if not dotnet: raise ValueError('Pinned .NET SDK is required; set DOTNET_HOST_PATH.')
project_file = 'JobTrackerApi.MariaDb.Tests/JobTrackerApi.MariaDb.Tests.csproj'
code = run([dotnet, 'restore', project_file, '--locked-mode'], 'restore.log')
if code == 0:
code = run([dotnet, 'test', project_file, '--configuration', 'Release', '--no-restore',
'--logger', 'trx;LogFileName=mariadb.trx', '--results-directory', str(output)], 'tests.log')
except Exception:
code = 1
raise
finally:
if managed:
# Never use down/-v: this generated project only owns its disposable test service.
run(compose + ['logs', '--no-color', 'mariadb-test'], 'server.log')
cleanup = run(compose + ['rm', '--stop', '--force', 'mariadb-test'], 'compose-cleanup.log')
if cleanup:
print('Test container cleanup failed; inspect compose-cleanup.log.')
if code == 0: code = cleanup
record = {'startedUtc':started, 'finishedUtc':datetime.datetime.now(datetime.timezone.utc).isoformat(),
'mode':'compose' if managed else 'explicit-existing-loopback', 'expectedVersion':environment.get('JOBTRACKER_MARIADB_TEST_VERSION','12.2.2'),
'project':project if managed else None, 'exitCode':code, 'commands':commands}
(output / 'run.json').write_text(json.dumps(record, indent=2)+'\n', encoding='utf-8')
print(json.dumps({'exitCode':code, 'evidence':str(output), 'mode':record['mode']}))
return code
if __name__ == '__main__':
try: raise SystemExit(main())
except ValueError as error: raise SystemExit(str(error))