build(core): enable and start Tropic emulator by default
What changed, and why it matters
This commit changes how the Trezor firmware build and test tooling handles the Tropic secure-element emulator. Previously, Tropic support was disabled by default in Unix emulator builds and had to be explicitly enabled; now it is enabled by default and the test harness starts the Tropic model automatically. It also refactors port allocation so each emulator worker reserves a wider block of ports. The changes are confined to build scripts, CI workflows, and test/development tooling. There is no direct fix for a runtime vulnerability in shipped firmware, but enabling a hardware-security emulator by default could theoretically expose test-only code paths or secrets if the emulator is misused outside of testing.
Treat as a routine build/test refactoring. Review whether enabling Tropic by default in emulator builds could cause test-only keys/configs to be loaded in non-test environments, and ensure the Tropic emulator is never reachable in production or release builds. No immediate security patch is indicated.
Security signals we found
Default-disabled security component (Tropic emulator) is now enabled by default in emulator builds
Test harness now auto-starts an external model_server process for Tropic-capable models
Port allocation widened to include a dedicated Tropic port per worker
Log files changed from overwrite ('w') to append ('a') mode
CI workflows remove explicit DISABLE_TROPIC=0 and manual model_server launch steps
Evidence from the diff
The patch flips DISABLE_TROPIC default from 1 to 0 in core/SConscript.unix, turns the –disable-tropic xtask flag from an optional bool into a plain checkbox flag, and updates CI to stop manually launching model_server steps (the emulator/test harness now starts it). It refactors TropicModel lifecycle management: CoreEmulator no longer owns/starts/stops the model; instead tests and core/emu.py instantiate TropicModel explicitly. Port math changes from 6 to 7 ports per worker, adding a dedicated tropic port offset. Log files are opened in append mode. The commit is purely build/test infrastructure; no firmware cryptographic code is modified.
Changed components
core/SConscript.unixcore/Makefilecore/embed/xtask/src/args.rscore/embed/xtask/src/feature_resolver.rscore/embed/xtask/tf-tools/manifest.yamlcore/emu.pypython/src/trezorlib/_internal/emulator.pytests/conftest.pytests/emulators.pytests/upgrade_tests/conftest.pytests/upgrade_tests/test_tropic_config_set.py.github/workflows/core.yml.github/workflows/crowdin-ui-check.yml.github/workflows/release-emu.ymlInspect captured patch +303 / −301
diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index 1f2547eb..220695e4 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -133,7 +133,6 @@ jobs:
N4W1: ${{ matrix.n4w1 && '1' || '0' }}
ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }}
LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt"
- DISABLE_TROPIC: 0
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
with:
@@ -144,9 +143,6 @@ jobs:
- run: nix-shell --run "uv run make -C core build_prodtest_emu"
if: matrix.coins == 'universal' && matrix.type != 'debuglink' && matrix.model == 'T3W1'
- run: nix-shell --run "uv run make -C core build_unix_frozen"
- - name: Start Tropic model
- if: ${{ matrix.model == 'T3W1' }}
- run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/config.yml > tests/trezor-tropic-model.log 2>&1 &"
- run: nix-shell --run "uv run make -C core test_emu_sanity"
- run: cp core/build-xtask/artifacts/latest/firmware-emu core/build-xtask/artifacts/latest/firmware-emu-${{ matrix.model }}-${{ matrix.coins }}${{ matrix.n4w1 && '-n4w1' || '' }}
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # actions/upload-artifact@v7.0.0
@@ -216,7 +212,6 @@ jobs:
BITCOIN_ONLY: ${{ matrix.coins == 'universal' && '0' || '1' }}
ADDRESS_SANITIZER: ${{ matrix.asan == 'asan' && '1' || '0' }}
LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt"
- DISABLE_TROPIC: 0
PYOPT: 0
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
@@ -224,11 +219,11 @@ jobs:
submodules: recursive
- uses: ./.github/actions/environment
- run: nix-shell --run "uv run make -C core build_unix"
+ - run: nix-shell --run "uv run make -C core test_emu_sanity" # sanity check non-frozen emulator
- name: Start Tropic model
if: ${{ matrix.model == 'T3W1' }}
run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/config.yml > tests/trezor-tropic-model.log 2>&1 &"
- run: nix-shell --run "uv run make -C core test"
- - run: nix-shell --run "uv run make -C core test_emu_sanity" # sanity check non-frozen emulator
core_unit_rust_test:
name: Rust unit tests (${{ matrix.model }}, ${{ matrix.asan }})
@@ -247,6 +242,7 @@ jobs:
RUSTC_BOOTSTRAP: ${{ matrix.asan == 'asan' && '1' || '0' }}
RUSTFLAGS: ${{ matrix.asan == 'asan' && '-Z sanitizer=address' || '' }}
LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt"
+ DISABLE_TROPIC: 1
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
with:
@@ -324,9 +320,6 @@ jobs:
path: core/build-xtask/artifacts/latest
- run: chmod +x core/build-xtask/artifacts/latest/firmware-emu*
- uses: ./.github/actions/environment
- - name: Start Tropic model
- if: ${{ env.TREZOR_MODEL == 'T3W1' && env.ACTIONS_DO_UI_TEST != 'true' }} # ACTIONS_DO_UI_TEST refers to the test_emu_ui_multicore below which uses --control-emulators and starts tvl internally
- run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/config.yml > tests/trezor-tropic-model.log 2>&1 &"
- name: Run device tests
if: ${{ !matrix.n4w1 }}
run: nix-shell --run "uv run make -C core ${{ env.ACTIONS_DO_UI_TEST == 'true' && 'test_emu_ui_multicore' || 'test_emu' }}"
@@ -474,9 +467,6 @@ jobs:
path: core/build-xtask/artifacts/latest
- run: chmod +x core/build-xtask/artifacts/latest/firmware-emu*
- uses: ./.github/actions/environment
- - name: Start Tropic model
- if: ${{ matrix.model == 'T3W1' }}
- run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/config.yml > tests/trezor-tropic-model.log 2>&1 &"
- run: nix-shell --run "uv run make -C core test_emu_persistence_ui"
if: ${{ matrix.asan == 'noasan' }}
- run: nix-shell --run "uv run make -C core test_emu_persistence"
@@ -641,9 +631,6 @@ jobs:
path: core/build-xtask/artifacts/latest
- run: chmod +x core/build-xtask/artifacts/latest/firmware-emu*
- uses: ./.github/actions/environment
- - name: Start Tropic model
- if: ${{ matrix.model == 'T3W1' }}
- run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/config.yml > tests/trezor-tropic-model.log 2>&1 &"
- run: nix-shell --run "uv run make -C tests/fido_tests/u2f-tests-hid"
- run: nix-shell --run "uv run make -C core test_emu_u2f"
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # actions/upload-artifact@v7.0.0
@@ -679,9 +666,6 @@ jobs:
path: core/build-xtask/artifacts/latest
- run: chmod +x core/build-xtask/artifacts/latest/firmware-emu*
- uses: ./.github/actions/environment
- - name: Start Tropic model
- if: ${{ matrix.model == 'T3W1' }}
- run: nix-shell --run "uv run model_server tcp -c tests/tropic_model/config.yml > tests/trezor-tropic-model.log 2>&1 &"
- run: nix-shell --run "uv run make -C core test_emu_fido2"
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # actions/upload-artifact@v7.0.0
with:
diff --git a/.github/workflows/crowdin-ui-check.yml b/.github/workflows/crowdin-ui-check.yml
index 302d121d..7d3fb399 100644
--- a/.github/workflows/crowdin-ui-check.yml
+++ b/.github/workflows/crowdin-ui-check.yml
@@ -35,7 +35,6 @@ jobs:
PYOPT: 0
ADDRESS_SANITIZER: 0
QUIET_MODE: 1
- DISABLE_TROPIC: 0
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
with:
diff --git a/.github/workflows/release-emu.yml b/.github/workflows/release-emu.yml
index 96f57906..de49f9b4 100644
--- a/.github/workflows/release-emu.yml
+++ b/.github/workflows/release-emu.yml
@@ -81,6 +81,7 @@ jobs:
TREZOR_MODEL: ${{ matrix.model }}
BITCOIN_ONLY: ${{ matrix.coins == 'universal' && '0' || '1' }}
PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }}
+ DISABLE_TROPIC: 1
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
with:
@@ -106,6 +107,7 @@ jobs:
TREZOR_MODEL: ${{ matrix.model }}
BITCOIN_ONLY: ${{ matrix.coins == 'universal' && '0' || '1' }}
PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }}
+ DISABLE_TROPIC: 1
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
with:
@@ -132,7 +134,6 @@ jobs:
TREZOR_MODEL: ${{ matrix.model }}
BITCOIN_ONLY: ${{ matrix.coins == 'universal' && '0' || '1' }}
PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }}
- DISABLE_TROPIC: "0"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
with:
@@ -159,7 +160,6 @@ jobs:
TREZOR_MODEL: ${{ matrix.model }}
BITCOIN_ONLY: ${{ matrix.coins == 'universal' && '0' || '1' }}
PYOPT: ${{ matrix.type == 'debuglink' && '0' || '1' }}
- DISABLE_TROPIC: "0"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6.0.2
with:
diff --git a/core/Makefile b/core/Makefile
index 0f514b7d..67173809 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -62,16 +62,14 @@ endif
ifeq ($(STORAGE_INSECURE_TESTING_MODE),1)
XTASK_BUILD_OPTS += --storage-insecure-testing-mode
-XTASK_BUILD_OPTS += --disable-tropic true
+XTASK_BUILD_OPTS += --disable-tropic
XTASK_BUILD_OPTS += --disable-optiga
else
ifeq ($(DISABLE_OPTIGA),1)
XTASK_BUILD_OPTS += --disable-optiga
endif
ifeq ($(DISABLE_TROPIC),1)
- XTASK_BUILD_OPTS += --disable-tropic true
- else ifeq ($(DISABLE_TROPIC),0)
- XTASK_BUILD_OPTS += --disable-tropic false
+ XTASK_BUILD_OPTS += --disable-tropic
endif
endif
diff --git a/core/SConscript.unix b/core/SConscript.unix
index 04febee5..ac5ce199 100644
--- a/core/SConscript.unix
+++ b/core/SConscript.unix
@@ -12,7 +12,7 @@ BOOTLOADER_DEVEL = ARGUMENTS.get('BOOTLOADER_DEVEL', '0') == '1'
CMAKELISTS = int(ARGUMENTS.get('CMAKELISTS', 0))
HW_REVISION ='emulator'
THP = ARGUMENTS.get('THP', '0') == '1' # Trezor-Host Protocol
-DISABLE_TROPIC = ARGUMENTS.get('DISABLE_TROPIC', '1') == '1'
+DISABLE_TROPIC = ARGUMENTS.get('DISABLE_TROPIC', '0') == '1'
BENCHMARK = ARGUMENTS.get('BENCHMARK', '0') == '1'
PYOPT = ARGUMENTS.get('PYOPT', '1')
DEBUGLINK = ARGUMENTS.get('DEBUGLINK', '0') == '1'
diff --git a/core/embed/xtask/src/args.rs b/core/embed/xtask/src/args.rs
index 7ca5f5f6..78524662 100644
--- a/core/embed/xtask/src/args.rs
+++ b/core/embed/xtask/src/args.rs
@@ -252,8 +252,8 @@ pub struct BuildArgs {
pub board: Option<String>,
/// Disable TROPIC support
- #[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub disable_tropic: Option<bool>,
+ #[arg(long)]
+ pub disable_tropic: bool,
/// Enable insecure storage test mode
#[arg(long)]
diff --git a/core/embed/xtask/src/feature_resolver.rs b/core/embed/xtask/src/feature_resolver.rs
index 67ae0a89..dd002050 100644
--- a/core/embed/xtask/src/feature_resolver.rs
+++ b/core/embed/xtask/src/feature_resolver.rs
@@ -159,7 +159,7 @@ pub fn resolve_features(args: &BuildArgs) -> Result<ResolvedBuild> {
if args.disable_optiga {
board_feat.retain(|f| f != "optiga");
}
- if args.disable_tropic.unwrap_or(args.emulator) {
+ if args.disable_tropic {
board_feat.retain(|f| f != "tropic");
}
features.extend(board_feat);
diff --git a/core/embed/xtask/tf-tools/manifest.yaml b/core/embed/xtask/tf-tools/manifest.yaml
index c9c606f2..46c4474a 100644
--- a/core/embed/xtask/tf-tools/manifest.yaml
+++ b/core/embed/xtask/tf-tools/manifest.yaml
@@ -266,18 +266,7 @@ options:
description: Controls whether TROPIC support is disabled.
when: component(firmware)
group: Testing
- type: multistate
- states:
- - value: null
- name: Default
- description: Use the target default.
- default: true
- - value: "true"
- name: Disabled
- description: Disable TROPIC support.
- - value: "false"
- name: Enabled
- description: Keep TROPIC support enabled.
+ type: checkbox
- id: storage-insecure-testing-mode
name: Insecure Storage Testing
diff --git a/core/emu.py b/core/emu.py
index 5632a7c2..5c968c20 100755
--- a/core/emu.py
+++ b/core/emu.py
@@ -15,7 +15,7 @@ import click
import trezorlib.debuglink
from trezorlib.cli.debug import record_screen
-from trezorlib._internal.emulator import CoreEmulator
+from trezorlib._internal.emulator import CoreEmulator, TropicModel
try:
import inotify.adapters
@@ -25,6 +25,7 @@ except Exception:
HERE = Path(__file__).resolve().parent
MICROPYTHON = HERE / "build-xtask" / "artifacts" / "latest" / "firmware-emu"
+TROPIC_MODEL_CONFIG = HERE.parent / "tests" / "tropic_model" / "config.yml"
SRC_DIR = HERE / "src"
PROFILE_BASE = Path.home() / ".trezoremu"
@@ -72,6 +73,7 @@ def run_debugger(
gdb_script_file: str | Path | None,
valgrind: bool = False,
run_command: list[str] = [],
+ tropic_model: TropicModel | None = None,
) -> None:
os.chdir(emulator.workdir)
env = emulator.make_env()
@@ -101,6 +103,8 @@ def run_debugger(
dbg_command += emulator.make_args()
if not run_command:
+ if tropic_model and tropic_model.process and tropic_model.process.pid:
+ click.echo(f"Warning: Tropic01 model (PID {tropic_model.process.pid}) left running", err=True)
os.execvpe(dbg_command[0], dbg_command, env)
else:
dbg_process = subprocess.Popen(dbg_command, env=env)
@@ -138,8 +142,10 @@ def _from_env(name: str) -> bool:
@click.option("-r", "--record-dir", help="Directory where to record screen changes", type=click.Path(file_okay=False, dir_okay=True, path_type=Path))
@click.option("-s", "--slip0014", is_flag=True, help="Initialize device with SLIP-14 seed (all all all...)")
@click.option("-S", "--script-gdb-file", type=click.Path(exists=True, dir_okay=False), help="Run gdb with an init file")
-@click.option("-V", "--valgrind", is_flag=True, help="Use valgrind instead of debugger (-D)")
@click.option("-t", "--temporary-profile", is_flag=True, help="Create an empty temporary profile")
+@click.option("--tropic-emulator/--no-tropic-emulator", default=True, help="Start Tropic01 model")
+@click.option("--tropic-emulator-config", type=click.Path(exists=True, dir_okay=False), default=TROPIC_MODEL_CONFIG, help="Tropic01 model configuration file")
+@click.option("-V", "--valgrind", is_flag=True, help="Use valgrind instead of debugger (-D)")
@click.option("-w", "--watch", is_flag=True, help="Restart emulator if sources change")
@click.option("-X", "--extra-arg", "extra_args", multiple=True, help="Extra argument to pass to micropython")
# fmt: on
@@ -165,8 +171,10 @@ def cli(
record_dir: Path | None,
slip0014: bool,
script_gdb_file: str | Path | None,
- valgrind: bool,
temporary_profile: bool,
+ tropic_emulator: bool,
+ tropic_emulator_config: Path,
+ valgrind: bool,
watch: bool,
extra_args: list[str],
command: list[str],
@@ -270,13 +278,14 @@ def cli(
heap_size=heap_size,
disable_animation=disable_animation,
workdir=SRC_DIR,
+ tropic_model_port=TropicModel.DEFAULT_PORT,
)
emulator_env = dict(
TREZOR_PATH=f"udp:127.0.0.1:{emulator.port}",
TREZOR_PROFILE_DIR=str(profile_dir.resolve()),
TREZOR_UDP_PORT=str(emulator.port),
- TREZOR_FIDO2_UDP_PORT=str(emulator.port + 2),
+ TREZOR_FIDO2_UDP_PORT=str(emulator.fido2_port()),
TREZOR_SRC=str(SRC_DIR),
)
os.environ.update(emulator_env)
@@ -289,8 +298,20 @@ def cli(
if alloc_profiling:
os.environ["TREZOR_MEMPERF"] = "1"
+ tropic_model = None
+ if tropic_emulator:
+ tropic_model = TropicModel(
+ profile_dir=str(profile_dir),
+ configfile=tropic_emulator_config,
+ port=TropicModel.DEFAULT_PORT,
+ )
+ try:
+ tropic_model.start()
+ except Exception as exc:
+ click.echo(f"Failed to start Tropic01 model: {exc.__class__.__name__}: {exc}", err=True)
+
if debugger or valgrind:
- run_debugger(emulator, script_gdb_file, valgrind, command)
+ run_debugger(emulator, script_gdb_file, valgrind, command, tropic_model)
raise RuntimeError("run_debugger should not return")
emulator.start()
@@ -323,6 +344,12 @@ def cli(
else:
ret = run_emulator(emulator)
+ if tropic_model:
+ try:
+ tropic_model.stop()
+ except Exception as exc:
+ click.echo(f"Error stopping Tropic01 model: {exc.__class__.__name__}: {exc}", err=True)
+
if tempdir is not None:
tempdir.cleanup()
sys.exit(ret)
diff --git a/python/src/trezorlib/_internal/emulator.py b/python/src/trezorlib/_internal/emulator.py
index f6161810..d3577900 100644
--- a/python/src/trezorlib/_internal/emulator.py
+++ b/python/src/trezorlib/_internal/emulator.py
@@ -53,19 +53,23 @@ def _rm_f(path: Path) -> None:
class TropicModel:
+ DEFAULT_PORT = 28992
+
def __init__(
self,
- workdir: Path,
- profile_dir: Path,
- port: int,
- configfile: str,
- logfile: Union[TextIO, str, Path],
+ profile_dir: str,
+ configfile: Path,
+ port: int = DEFAULT_PORT,
+ logfile: TextIO | str | Path | None = None,
+ configfile_output: Path | None = None,
) -> None:
- self.workdir = workdir
- self.profile_dir = profile_dir
+ self.profile_dir = Path(profile_dir).resolve()
self.port = port
- self.configfile = configfile
- self.logfile = logfile
+ self.configfile = configfile.resolve()
+ self.configfile_output = (
+ configfile_output or self.profile_dir / "tropic_model_config_output.yml"
+ )
+ self.logfile = logfile or self.profile_dir / "trezor-tropic-model.log"
self.process: Optional[subprocess.Popen] = None
def start(self) -> None:
@@ -101,27 +105,27 @@ class TropicModel:
output = self.logfile
else:
assert isinstance(self.logfile, (str, Path))
- output = open(self.logfile, "w")
+ output = open(self.logfile, "a")
return subprocess.Popen(
[
"model_server",
"tcp",
"-c",
- self.configfile,
+ str(self.configfile),
"-p",
str(self.port),
"-o",
- str(self.profile_dir / "tropic_model_config_output.yml"),
+ str(self.configfile_output),
],
- cwd=self.workdir,
+ cwd=self.profile_dir,
stdout=cast(TextIO, output),
stderr=subprocess.STDOUT,
)
def _wait_until_ready(self, timeout: float = TROPIC_MODEL_WAIT_TIME) -> None:
assert self.process is not None, "Tropic model not started"
- LOG.info("Waiting for Tropic model to come up...")
+ LOG.info(f"Waiting for Tropic model to come up on port {self.port}...")
start = time.monotonic()
while True:
try:
@@ -144,7 +148,13 @@ class TropicModel:
time.sleep(0.1)
- LOG.info(f"Emulator ready after {time.monotonic() - start:.3f} seconds")
+ LOG.info(f"Tropic model ready after {time.monotonic() - start:.3f} seconds")
+
+ def __enter__(self) -> "TropicModel":
+ return self
+
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+ self.stop()
class Emulator:
@@ -196,12 +206,6 @@ class Emulator:
# To save all screenshots properly in one directory between restarts
self.restart_amount = 0
- def start_tropic_model(self) -> None:
- pass
-
- def stop_tropic_model(self) -> None:
- pass
-
@property
def client(self) -> TrezorTestContext:
"""So that type-checkers do not see `client` as `Optional`.
@@ -260,7 +264,7 @@ class Emulator:
output = self.logfile
else:
assert isinstance(self.logfile, (str, Path))
- output = open(self.logfile, "w")
+ output = open(self.logfile, "a")
return subprocess.Popen(
[str(self.executable)] + args + self.extra_args,
@@ -284,8 +288,6 @@ class Emulator:
# process is running, no need to start again
return
- self.start_tropic_model()
-
self.transport = transport or self._get_transport()
self.process = self._launch_process()
_RUNNING_PIDS.add(self.process)
@@ -328,8 +330,6 @@ class Emulator:
self.process.kill()
_RUNNING_PIDS.remove(self.process)
- self.stop_tropic_model()
-
_rm_f(self.profile_dir / "trezor.pid")
_rm_f(self.profile_dir / "trezor.port")
self.process = None
@@ -362,10 +362,7 @@ class CoreEmulator(Emulator):
def __init__(
self,
*args: Any,
- launch_tropic_model: bool = False,
tropic_model_port: Optional[int] = None,
- tropic_model_configfile: Optional[str] = None,
- tropic_model_logfile: Union[TextIO, str, Path, None] = None,
port: Optional[int] = None,
main_args: Sequence[str] = ("-m", "main"),
workdir: Optional[Path] = None,
@@ -382,37 +379,14 @@ class CoreEmulator(Emulator):
if sdcard is not None:
self.sdcard.write_bytes(sdcard)
- self.tropic_model_port = tropic_model_port
-
- if launch_tropic_model:
- assert tropic_model_port
- assert tropic_model_configfile
- self.tropic_model = TropicModel(
- workdir=self.workdir,
- profile_dir=self.profile_dir,
- port=tropic_model_port,
- configfile=tropic_model_configfile,
- logfile=(
- tropic_model_logfile or self.profile_dir / "trezor-tropic-model.log"
- ),
- )
- else:
- self.tropic_model = None
-
if port:
self.port = port
+ self.tropic_model_port = tropic_model_port
+
self.disable_animation = disable_animation
self.main_args = list(main_args)
self.heap_size = heap_size
- def start_tropic_model(self) -> None:
- if self.tropic_model:
- self.tropic_model.start()
-
- def stop_tropic_model(self) -> None:
- if self.tropic_model:
- self.tropic_model.stop()
-
def make_env(self) -> Dict[str, str]:
env = super().make_env()
env.update(
@@ -425,8 +399,7 @@ class CoreEmulator(Emulator):
if self.headless or self.disable_animation:
env["TREZOR_DISABLE_FADE"] = "1"
env["TREZOR_DISABLE_ANIMATION"] = "1"
- if self.tropic_model_port is not None:
- env["TROPIC_MODEL_PORT"] = str(self.tropic_model_port)
+ env["TROPIC_MODEL_PORT"] = str(self.tropic_port())
return env
@@ -438,6 +411,24 @@ class CoreEmulator(Emulator):
+ self.extra_args
)
+ # UDP ports are hardcoded as offsets to the base wirelink port
+ def debuglink_port(self) -> int:
+ return self.port + 1
+
+ def fido2_port(self) -> int:
+ return self.port + 2
+
+ def vcp_port(self) -> int:
+ return self.port + 3
+
+ def ble_port(self) -> tuple[int, int]:
+ return (self.port + 4, self.port + 5)
+
+ # Tropic model can be managed externally, return configured port if set.
+ # Also TCP instead of UDP.
+ def tropic_port(self) -> int:
+ return self.tropic_model_port or (self.port + 6)
+
class LegacyEmulator(Emulator):
STORAGE_FILENAME = "emulator.img"
@@ -447,9 +438,3 @@ class LegacyEmulator(Emulator):
if self.headless:
env["SDL_VIDEODRIVER"] = "dummy"
return env
-
- def start_tropic_model(self) -> None:
- pass
-
- def stop_tropic_model(self) -> None:
- pass
diff --git a/tests/conftest.py b/tests/conftest.py
index 482fce19..fdbd432d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -18,6 +18,7 @@ from __future__ import annotations
import logging
import os
+import tempfile
import typing as t
from dataclasses import asdict, dataclass
from enum import IntEnum
@@ -30,6 +31,7 @@ from _pytest.reports import TestReport
from trezorlib import client as client_module
from trezorlib import debuglink, log, messages, models
+from trezorlib._internal.emulator import TropicModel
from trezorlib.debuglink import TrezorTestContext
from trezorlib.device import apply_settings
from trezorlib.transport import enumerate_devices, get_transport
@@ -43,7 +45,13 @@ from trezorlib.testing import translations
from trezorlib.testing.device_handler import BackgroundDeviceHandler
from . import ui_tests
-from .emulators import EmulatorWrapper
+from .emulators import (
+ TROPIC_MODEL_CONFIGFILE,
+ EmulatorWrapper,
+ delete_profile,
+ get_logfile,
+ get_tropic_model_port,
+)
if t.TYPE_CHECKING:
from _pytest.config import Config
@@ -79,10 +87,46 @@ def _emulator_wrapper_main_args() -> list[str]:
return ["-m", "main"]
+def _get_worker_id(request: pytest.FixtureRequest) -> int:
+ worker_id = xdist.get_xdist_worker_id(request)
+ if worker_id == "master":
+ return 0
+ assert worker_id.startswith("gw")
+ return 1 + int(worker_id[2:])
+
+
+@pytest.fixture(scope="session")
+def tropic_model(request: pytest.FixtureRequest) -> t.Iterator[TropicModel]:
+ worker_id = _get_worker_id(request)
+ logfile = get_logfile(f"trezor-tropic-model-{worker_id}.log")
+ port = get_tropic_model_port(worker_id)
+
+ with tempfile.TemporaryDirectory(
+ prefix="trezor-tropic-model-", delete=delete_profile()
+ ) as temp_dir:
+ LOG.debug(
+ f"Tropic model workdir: {temp_dir} (delete: {delete_profile()}), port: {port}, log: {logfile}"
+ )
+ with TropicModel(
+ profile_dir=temp_dir,
+ configfile=TROPIC_MODEL_CONFIGFILE,
+ port=port,
+ logfile=logfile,
+ ) as tropic_model:
+ tropic_model.start()
+ yield tropic_model
+
+
@pytest.fixture
-def core_emulator(request: pytest.FixtureRequest) -> t.Iterator[Emulator]:
+def core_emulator(
+ tropic_model: TropicModel, request: pytest.FixtureRequest
+) -> t.Iterator[Emulator]:
"""Fixture returning default core emulator with possibility of screen recording."""
- with EmulatorWrapper("core", main_args=_emulator_wrapper_main_args()) as emu:
+ with EmulatorWrapper(
+ "core",
+ main_args=_emulator_wrapper_main_args(),
+ tropic_model_port=tropic_model.port,
+ ) as emu:
# Modifying emu.client to add screen recording (when --ui=test is used)
_check_protocol(request, emu.client)
with ui_tests.screen_recording(emu.client, request, lambda: emu.client) as _:
@@ -90,7 +134,9 @@ def core_emulator(request: pytest.FixtureRequest) -> t.Iterator[Emulator]:
@pytest.fixture(scope="session")
-def emulator(request: pytest.FixtureRequest) -> t.Generator["Emulator", None, None]:
+def emulator(
+ tropic_model: TropicModel, request: pytest.FixtureRequest
+) -> t.Generator["Emulator", None, None]:
"""Fixture for getting emulator connection in case tests should operate it on their own.
Is responsible for starting it at the start of the session and stopping
@@ -119,17 +165,13 @@ def emulator(request: pytest.FixtureRequest) -> t.Generator["Emulator", None, No
"Legacy emulator is not supported until it can be run on arbitrary ports."
)
- worker_id = xdist.get_xdist_worker_id(request)
- assert worker_id.startswith("gw")
- worker_id = int(worker_id[2:])
-
with EmulatorWrapper(
model,
- worker_id=worker_id,
+ worker_id=_get_worker_id(request),
headless=True,
auto_interact=not interact,
main_args=_emulator_wrapper_main_args(),
- launch_tropic_model=True,
+ tropic_model_port=tropic_model.port,
) as emu:
yield emu
diff --git a/tests/emulators.py b/tests/emulators.py
index 255780f4..d1bca6bb 100644
--- a/tests/emulators.py
+++ b/tests/emulators.py
@@ -15,25 +15,21 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
from __future__ import annotations
+import logging
import os
import tempfile
from collections import defaultdict
from pathlib import Path
-from typing import Dict, Sequence, Tuple
-
-from trezorlib._internal.emulator import (
- CoreEmulator,
- Emulator,
- LegacyEmulator,
- TropicModel,
-)
+from typing import Sequence, Tuple
+
+from trezorlib._internal.emulator import CoreEmulator, Emulator, LegacyEmulator
from trezorlib.models import CORE_MODELS, LEGACY_MODELS
+LOG = logging.getLogger(__name__)
+
ROOT = Path(__file__).resolve().parent.parent
BINDIR = ROOT / "tests" / "emulators"
-_SHARED_TROPIC_MODELS: Dict[str, TropicModel] = {}
-
LOCAL_BUILD_PATHS = {
"core": ROOT / "core" / "build-xtask" / "artifacts" / "latest" / "firmware-emu",
"legacy": ROOT / "legacy" / "firmware" / "trezor.elf",
@@ -43,10 +39,10 @@ CORE_SRC_DIR = ROOT / "core" / "src"
ENV = {"SDL_VIDEODRIVER": "dummy"}
-TROPIC_MODEL_CONFIGFILE_OLD = ROOT / "tests" / "tropic_model" / "config_old.yml"
TROPIC_MODEL_CONFIGFILE = ROOT / "tests" / "tropic_model" / "config.yml"
-TROPIC_CAPABLE_MODELS = {"T3W1"}
+TROPIC_MODEL_CONFIGFILE_OLD = ROOT / "tests" / "tropic_model" / "config_old.yml"
TROPIC_OLD_CONFIG_UNTIL_VERSION = (2, 12, 1) # inclusive
+TROPIC_CAPABLE_MODELS = {"T3W1"}
def is_tropic_capable_model(model_internal_name: str | None) -> bool:
@@ -71,35 +67,6 @@ def gen_from_model(model_internal_name: str) -> str:
raise ValueError(f"Unknown model: {model_internal_name}")
-def _get_shared_tropic_model(
- profile_dir: str,
- workdir: Path | None,
- port: int,
- configfile: Path,
- logfile: Path | None,
-) -> TropicModel:
- model = _SHARED_TROPIC_MODELS.get(profile_dir)
- if model is None or model.process is None or model.process.poll() is not None:
- if model is not None:
- model.stop()
- model = TropicModel(
- workdir=workdir or ROOT,
- profile_dir=Path(profile_dir),
- port=port,
- configfile=str(configfile),
- logfile=logfile or (Path(profile_dir) / "trezor-tropic-model.log"),
- )
- model.start()
- _SHARED_TROPIC_MODELS[profile_dir] = model
- return model
-
-
-def stop_shared_tropic_model(profile_dir: str) -> None:
- model = _SHARED_TROPIC_MODELS.pop(profile_dir, None)
- if model:
- model.stop()
-
-
def check_version(tag: str, version_tuple: Tuple[int, int, int]) -> None:
if tag is not None and tag.startswith("v") and len(tag.split(".")) == 3:
version = ".".join(str(i) for i in version_tuple)
@@ -163,34 +130,30 @@ def get_tags() -> dict[str, list[str]]:
ALL_TAGS = get_tags()
-def _get_tropic_model_port(worker_id: int) -> int:
- """Get a unique port for this worker process' Tropic model.
+def get_tropic_model_port(worker_id: int) -> int:
+ """Get a unique port for this worker process' shared Tropic model.
Guarantees to be unique because each worker has a unique ID.
"""
- return 28992 + worker_id # 28992 is the default port tvl server listens to
+ if worker_id == 0:
+ # was not configurable before 2.9.4, use the default value for singlecore upgrade tests
+ return 28992
+ else:
+ return 20000 + worker_id * 7 + 6
def _get_port(worker_id: int) -> int:
"""Get a unique port for this worker process on which it can run.
Guarantees to be unique because each worker has a unique ID.
- #0=>20000, #1=>20003, #2=>20006, etc.
+ #0=>20000, #1=>20007, #2=>20014, etc.
"""
- # One emulator instance occupies 3 consecutive ports:
+ # One emulator instance occupies 7 consecutive ports:
# 1. normal link, 2. debug link and 3. webauthn fake interface
# 4. USB serial 5. ble-emulator-data 6. ble-emulator-events
- return 20000 + worker_id * 6
-
-
-def _get_tropic_model_configfile(tag: str | None) -> Path:
- if tag is not None and tag.startswith("v"):
- tag_version = tag[1:].partition("-")[0]
- if len(tag_version.split(".")) == 3:
- version_tuple = tuple(int(i) for i in tag_version.split("."))
- if version_tuple <= TROPIC_OLD_CONFIG_UNTIL_VERSION:
- return TROPIC_MODEL_CONFIGFILE_OLD
- return TROPIC_MODEL_CONFIGFILE
+ # 7. tropic model
+ # See: *_PORT_OFFSET constants in core sources
+ return 20000 + worker_id * 7
class EmulatorWrapper:
@@ -200,14 +163,12 @@ class EmulatorWrapper:
model: str | None,
tag: str | None = None,
storage: bytes | None = None,
- profile_dir: tempfile.TemporaryDirectory | None = None,
+ profile_dir: str | None = None,
worker_id: int = 0,
headless: bool = True,
auto_interact: bool = True,
main_args: Sequence[str] = ("-m", "main"),
- launch_tropic_model: bool | None = None,
- tropic_model_port_override: int | None = None,
- port_override: int | None = None,
+ tropic_model_port: int | None = None,
) -> None:
if model is None:
@@ -215,8 +176,8 @@ class EmulatorWrapper:
gen = gen_from_model(model)
- if launch_tropic_model is None:
- launch_tropic_model = is_tropic_capable_model(model)
+ if tropic_model_port is None:
+ tropic_model_port = get_tropic_model_port(worker_id)
if tag is not None:
executable = get_emulator_path(gen, model, tag)
@@ -226,60 +187,29 @@ class EmulatorWrapper:
if not executable.exists():
raise ValueError(f"emulator executable not found: {executable}")
- self.profile_dir = profile_dir or tempfile.TemporaryDirectory()
- self.own_profile_dir = profile_dir is None
+ if profile_dir:
+ self.temp_dir = None
+ self.profile_dir = profile_dir
+ else:
+ self.temp_dir = tempfile.TemporaryDirectory(
+ prefix="trezor-emulator-", delete=delete_profile()
+ )
+ self.profile_dir = self.temp_dir.name
+ LOG.debug(
+ f"Emulator profile dir: {self.profile_dir} (delete: {delete_profile()})"
+ )
+
if executable == LOCAL_BUILD_PATHS["core"]:
workdir = CORE_SRC_DIR
else:
workdir = None
- logs_dir = os.environ.get("TREZOR_PYTEST_LOGS_DIR")
- logfile = None
- tropic_model_logfile = None
- if logs_dir:
- logfile = Path(logs_dir) / f"trezor-{worker_id}.log"
- tropic_model_logfile = (
- Path(logs_dir) / f"trezor-tropic-model-{worker_id}.log"
- )
+ logfile = get_logfile(f"trezor-{worker_id}.log")
- tropic_configfile = _get_tropic_model_configfile(tag)
- if launch_tropic_model:
- tropic_config_output = (
- Path(self.profile_dir.name) / "tropic_model_config_output.yml"
- )
- if tropic_config_output.exists():
- tropic_configfile = tropic_config_output
-
- use_shared_tropic_model = launch_tropic_model and not self.own_profile_dir
- launch_tropic_model_for_emulator = launch_tropic_model
- if use_shared_tropic_model:
- shared_model = _get_shared_tropic_model(
- profile_dir=self.profile_dir.name,
- workdir=workdir,
- port=(
- tropic_model_port_override
- if tropic_model_port_override is not None
- else _get_tropic_model_port(worker_id)
- ),
- configfile=tropic_configfile,
- logfile=(
- tropic_model_logfile
- if isinstance(tropic_model_logfile, Path)
- else None
- ),
- )
- launch_tropic_model_for_emulator = False
- tropic_model_port = shared_model.port
- else:
- tropic_model_port = (
- tropic_model_port_override
- if tropic_model_port_override is not None
- else _get_tropic_model_port(worker_id)
- )
if gen == "legacy":
self.emulator = LegacyEmulator(
executable,
- self.profile_dir.name,
+ self.profile_dir,
storage=storage,
headless=headless,
auto_interact=auto_interact,
@@ -288,16 +218,11 @@ class EmulatorWrapper:
elif gen == "core":
self.emulator = CoreEmulator(
executable,
- self.profile_dir.name,
+ self.profile_dir,
storage=storage,
workdir=workdir,
- launch_tropic_model=launch_tropic_model_for_emulator,
tropic_model_port=tropic_model_port,
- tropic_model_configfile=str(tropic_configfile),
- tropic_model_logfile=tropic_model_logfile,
- port=(
- port_override if port_override is not None else _get_port(worker_id)
- ),
+ port=_get_port(worker_id),
headless=headless,
auto_interact=auto_interact,
main_args=main_args,
@@ -314,5 +239,16 @@ class EmulatorWrapper:
def __exit__(self, exc_type, exc_value, traceback) -> None:
self.emulator.stop()
- if self.own_profile_dir:
- self.profile_dir.cleanup()
+ if self.temp_dir and not delete_profile():
+ self.temp_dir.cleanup()
+
+
+def get_logfile(filename: str, default_dir: Path | None = None) -> Path | None:
+ logs_dir = os.environ.get("TREZOR_PYTEST_LOGS_DIR") or default_dir
+ if logs_dir is None:
+ return None
+ return Path(logs_dir) / filename
+
+
+def delete_profile() -> bool:
+ return os.environ.get("TREZOR_KEEP_PROFILE_DIR") != "1"
diff --git a/tests/upgrade_tests/conftest.py b/tests/upgrade_tests/conftest.py
index 7ed7cbc9..97f7cf72 100644
--- a/tests/upgrade_tests/conftest.py
+++ b/tests/upgrade_tests/conftest.py
@@ -1,37 +1,63 @@
-import os
+import logging
import tempfile
+from pathlib import Path
+from typing import Any, Generator
import pytest
-from ..emulators import stop_shared_tropic_model
+from trezorlib._internal.emulator import TropicModel
+from ..emulators import (
+ TROPIC_MODEL_CONFIGFILE,
+ TROPIC_MODEL_CONFIGFILE_OLD,
+ TROPIC_OLD_CONFIG_UNTIL_VERSION,
+ delete_profile,
+ get_logfile,
+ is_tropic_capable_model,
+)
+LOG = logging.getLogger(__name__)
+
+
+def _get_tropic_model_configfile(tag: str | None) -> Path:
+ if tag is not None and tag.startswith("v"):
+ tag_version = tag[1:].partition("-")[0]
+ if len(tag_version.split(".")) == 3:
+ version_tuple = tuple(int(i) for i in tag_version.split("."))
+ if version_tuple <= TROPIC_OLD_CONFIG_UNTIL_VERSION:
+ return TROPIC_MODEL_CONFIGFILE_OLD
+ return TROPIC_MODEL_CONFIGFILE
+
+
+# This fixture is very similar to `tropic_model` from the parent directory, but has a "function"
+# scope instead of session.
@pytest.fixture
-def shared_profile_dir():
- keep_profile = os.environ.get("TREZOR_KEEP_PROFILE_DIR") == "1"
- profile_dir = tempfile.TemporaryDirectory()
- # TODO: in Python >=3.12, simplify to
- # with tempfile.TemporaryDirectory(delete=not keep_profile) as path:
- # yield path # str, not TemporaryDirectory
-
- if keep_profile:
- # Prevent automatic cleanup when the object is GC'd.
- finalizer = getattr(profile_dir, "_finalizer", None)
- if finalizer is not None:
- try:
- finalizer.detach()
- except AttributeError:
- pass
-
- try:
- yield profile_dir
- finally:
- if not keep_profile:
- profile_dir.cleanup()
-
-
-@pytest.fixture(autouse=True)
-def _cleanup_shared_tropic_model(shared_profile_dir):
- """Stop any shared Tropic model that was started during the test."""
- yield
- stop_shared_tropic_model(shared_profile_dir.name)
+def shared_profile_dir(request) -> Generator[str, Any, Any]:
+ # Use the default port because before 2.9.4 it was not configurable.
+ # This means upgrade tests currently can't run in multiple threads for T3W1.
+ tropic_model_port = 28992
+ model = request.node.callspec.params["model"]
+ start_tropic_model = is_tropic_capable_model(model)
+
+ profile_dir = tempfile.TemporaryDirectory(
+ prefix="trezor-upgrade-", delete=delete_profile()
+ )
+ LOG.debug(
+ f"Test profile dir: {profile_dir.name} (delete: {delete_profile()}), start_tropic: {start_tropic_model}"
+ )
+
+ with profile_dir as path:
+ # do not start tropic model when not supported
+ if not start_tropic_model:
+ yield path
+ return
+
+ tag = request.node.callspec.params["tag"]
+ with TropicModel(
+ profile_dir=path,
+ configfile=_get_tropic_model_configfile(tag),
+ port=tropic_model_port,
+ logfile=get_logfile("trezor-tropic-model.log", Path(profile_dir.name)),
+ ) as tropic_model:
+ tropic_model.start()
+ yield path
diff --git a/tests/upgrade_tests/test_firmware_upgrades.py b/tests/upgrade_tests/test_firmware_upgrades.py
index 132b434d..7cb9aada 100644
--- a/tests/upgrade_tests/test_firmware_upgrades.py
+++ b/tests/upgrade_tests/test_firmware_upgrades.py
@@ -97,7 +97,7 @@ def _get_session(client: "Client", passphrase: str | None = "") -> "Session":
def test_upgrade_load(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
) -> None:
def asserts(client: "Client"):
client.refresh_features()
@@ -140,7 +140,7 @@ def test_upgrade_load(
def test_upgrade_load_pin(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
) -> None:
PIN = "1234"
@@ -192,7 +192,9 @@ def test_upgrade_load_pin(
("T1B1", ["v1.8.0", "v1.9.0"]),
)
@lower_models_minimum_version
-def test_storage_upgrade_progressive(tags: List[str], model: str, shared_profile_dir):
+def test_storage_upgrade_progressive(
+ tags: List[str], model: str, shared_profile_dir: str
+):
PIN = "1234"
def asserts(client: "Client") -> None:
@@ -242,7 +244,7 @@ def test_storage_upgrade_progressive(tags: List[str], model: str, shared_profile
def test_upgrade_wipe_code(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
):
PIN = "1234"
WIPE_CODE = "4321"
@@ -302,7 +304,7 @@ def test_upgrade_wipe_code(
def test_upgrade_reset(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
):
def asserts(client: "Client"):
assert not client.features.pin_protection
@@ -347,7 +349,7 @@ def test_upgrade_reset(
def test_upgrade_reset_skip_backup(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
):
def asserts(client: "Client"):
assert not client.features.pin_protection
@@ -393,7 +395,7 @@ def test_upgrade_reset_skip_backup(
def test_upgrade_reset_no_backup(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
):
def asserts(client: "Client"):
assert not client.features.pin_protection
@@ -441,7 +443,7 @@ def test_upgrade_reset_no_backup(
def test_upgrade_shamir_recovery(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
):
with (
EmulatorWrapper(
@@ -538,7 +540,7 @@ def test_upgrade_shamir_recovery(
def test_upgrade_shamir_backup(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
):
with EmulatorWrapper(
model,
@@ -621,7 +623,7 @@ def test_upgrade_shamir_backup(
def test_upgrade_u2f(
tag: str | None,
model: str | None,
- shared_profile_dir,
+ shared_profile_dir: str,
):
"""Check U2F counter stayed the same after an upgrade."""
with EmulatorWrapper(
@@ -665,7 +667,7 @@ def test_cardano_address_does_not_change_by_upgrade(
model: str | None,
backup_type: BackupType,
derivation_type: CardanoDerivationType,
- shared_profile_dir,
+ shared_profile_dir: str,
):
"""
Check that the Cardano address does not change after upgrading app storage from v2
diff --git a/tests/upgrade_tests/test_passphrase_consistency.py b/tests/upgrade_tests/test_passphrase_consistency.py
index 49d76f82..fec3d4a1 100644
--- a/tests/upgrade_tests/test_passphrase_consistency.py
+++ b/tests/upgrade_tests/test_passphrase_consistency.py
@@ -43,7 +43,11 @@ mapping.DEFAULT_MAPPING.register(ApplySettingsCompat)
@pytest.fixture
-def emulator(tag: str, model: str, shared_profile_dir) -> Iterator[Emulator]:
+def emulator(
+ tag: str,
+ model: str,
+ shared_profile_dir: str,
+) -> Iterator[Emulator]:
with EmulatorWrapper(
model,
tag=tag,
diff --git a/tests/upgrade_tests/test_tropic_config_set.py b/tests/upgrade_tests/test_tropic_config_set.py
index ee691fee..3bb6f180 100644
--- a/tests/upgrade_tests/test_tropic_config_set.py
+++ b/tests/upgrade_tests/test_tropic_config_set.py
@@ -16,24 +16,28 @@
import json
-import socket
import tempfile
+from contextlib import nullcontext
from dataclasses import dataclass
from pathlib import Path
import pytest
import yaml
-from tests.emulators import LOCAL_BUILD_PATHS, ROOT, TROPIC_MODEL_CONFIGFILE
-from trezorlib._internal.emulator import CoreEmulator
+from tests.emulators import (
+ ROOT,
+ TROPIC_MODEL_CONFIGFILE,
+ EmulatorWrapper,
+ delete_profile,
+ get_logfile,
+)
+from trezorlib._internal.emulator import TropicModel
from . import model_only
-BUILD_PATH = LOCAL_BUILD_PATHS["core"]
TROPIC_CONFIGS_JSON = (
ROOT / "core" / "embed" / "sec" / "tropic" / "config" / "tropic_configs.json"
)
-CORE_SRC_DIR = ROOT / "core" / "src"
TROPIC_MODEL_DIR = TROPIC_MODEL_CONFIGFILE.parent
TROPIC_CONFIG_DISTRIBUTION_VERSION_SLOT = 6
TROPIC_CONFIG_BACKUP_DISTRIBUTION_VERSION_SLOT = 7
@@ -207,13 +211,6 @@ TROPIC_BOOT_SCENARIOS = [
]
-def _free_port() -> int:
- """Ask the OS to allocate a free TCP port by binding to port 0, then return it."""
- with socket.socket() as s:
- s.bind(("", 0))
- return s.getsockname()[1]
-
-
def _config_to_numbers(config: dict, irreversible: bool) -> dict[str, int]:
numbers = {}
for category, category_config in config.items():
@@ -362,27 +359,40 @@ def _check_tropic_model_output(
@model_only("T3W1")
@pytest.mark.parametrize("scenario", TROPIC_BOOT_SCENARIOS, ids=lambda s: s.id)
def test_tropic_boot(scenario: TropicBootScenario) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
+ with tempfile.TemporaryDirectory(
+ prefix="trezor-tropic-config-", delete=delete_profile()
+ ) as temp_dir:
config_path = Path(temp_dir) / "tropic_model_config.yml"
output_path = Path(temp_dir) / "tropic_model_config_output.yml"
config_path.write_text(
yaml.safe_dump(_build_tropic_model_config(scenario), sort_keys=False)
)
- with CoreEmulator(
+ # using the default port - needs similar mechanism as device tests if we want
+ # parallel execution of test cases (currently not supported for upgrade tests)
+ with TropicModel(
profile_dir=temp_dir,
- executable=BUILD_PATH,
- workdir=CORE_SRC_DIR,
- headless=True,
- launch_tropic_model=True,
- tropic_model_configfile=str(config_path),
- tropic_model_port=_free_port(),
- ) as emulator:
+ configfile=config_path,
+ configfile_output=output_path,
+ logfile=get_logfile("trezor-tropic-model.log", Path(temp_dir)),
+ ) as tropic_model:
+ tropic_model.start()
+
if scenario.expect_failure:
- with pytest.raises(RuntimeError, match="Emulator process died"):
- emulator.start()
+ expectation = pytest.raises(RuntimeError, match="Emulator process died")
else:
- emulator.start()
+ expectation = nullcontext()
+
+ with (
+ expectation,
+ EmulatorWrapper(
+ model="core",
+ profile_dir=temp_dir,
+ tropic_model_port=tropic_model.port,
+ ),
+ ):
+ # wait for start, then exit immediately
+ pass
if not scenario.expect_failure:
assert scenario.expected_i_version is not None
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.