summaryrefslogtreecommitdiff
path: root/test/tx/cmake/module_qemu/run_qemu_test.py
blob: b2533c5d2d7a7efecad4ca00ddd65ecfb32983fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python3
#
# Copyright (c) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
#
"""Run a module harness image on QEMU and assert its result.

Modelled on the Cortex-R52 FVP runner.  Every image reports through Arm
semihosting and ends the simulation itself with SYS_EXIT, so the runner only
launches QEMU, captures what the image printed, and judges it.  Both the
result line and QEMU's exit status must say the image passed.

A missing result line is a failure, never a pass: a hang, or a crash before
the first check, must not be able to look like success.
"""

import argparse
import subprocess
import sys

PASS_MARK = "ALL CHECKS PASSED"
FAIL_MARK = "RESULT: FAILED"
FAULT_MARK = "[FAULT]"


def as_text(stream):
    """Normalise a captured stream to str; a timeout hands back bytes."""
    if stream is None:
        return ""
    if isinstance(stream, bytes):
        return stream.decode(errors="replace")
    return stream


def run(qemu, machine, elf, timeout):
    command = [
        qemu,
        "-M", machine,
        "-nographic",
        "-monitor", "none",
        "-serial", "none",
        "-semihosting-config", "enable=on,target=native",
        "-kernel", elf,
    ]

    print("Running:", " ".join(command), flush=True)

    try:
        completed = subprocess.run(
            command, capture_output=True, text=True, timeout=timeout
        )
    except subprocess.TimeoutExpired as expired:
        print(as_text(expired.stdout) + as_text(expired.stderr))
        print(
            f"FAIL: QEMU did not terminate within {timeout}s. Every image is "
            "expected to exit through semihosting SYS_EXIT, so this is a hang.",
            file=sys.stderr,
        )
        return 1

    output = as_text(completed.stdout) + as_text(completed.stderr)
    print(output)

    if FAULT_MARK in output:
        print("FAIL: the image reported an unexpected exception.", file=sys.stderr)
        return 1
    if FAIL_MARK in output:
        print("FAIL: the image reported failing checks.", file=sys.stderr)
        return 1
    if PASS_MARK not in output:
        print("FAIL: the image printed no result line.", file=sys.stderr)
        return 1
    if completed.returncode != 0:
        print(
            f"FAIL: the image reported success but QEMU exited with "
            f"{completed.returncode}.",
            file=sys.stderr,
        )
        return 1

    return 0


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--qemu", required=True, help="qemu-system-arm executable")
    parser.add_argument("--machine", required=True, help="QEMU machine, e.g. mps2-an385")
    parser.add_argument("--elf", required=True, help="image to run")
    parser.add_argument("--timeout", type=int, default=60, help="seconds before a hang is declared")
    args = parser.parse_args()
    return run(args.qemu, args.machine, args.elf, args.timeout)


if __name__ == "__main__":
    sys.exit(main())