Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-ops/tests/streamlit_image_matches_requirements.rs

//! **The image and the requirements file must agree about what this app runs on.**
//!
//! `py/requirements.txt` is the one writer for the demo's runtime versions — it says
//! so, and it records that they were verified on this box. The Streamlit image pins
//! the same three as build ARGs, because installing the framework is a BUILD-time
//! step and the requirements file is a RUN-time bind mount, so the recipe cannot
//! read it.
//!
//! Two copies with no writer between them is the drift this repo has now paid for
//! twice in one day (the python version lived in four files; one of them was six
//! releases behind). FOUND on the first build of this image: it pinned streamlit
//! **1.42.2** while `requirements.txt` said **1.60.0** — a number I picked, silently
//! disagreeing with the one that had been verified. This test is why that lasted
//! minutes instead of weeks.

use std::path::{Path, PathBuf};

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf()
}

/// `name==version` out of a pip requirements file.
fn required(name: &str) -> String {
    let p = repo_root().join("py/requirements.txt");
    let text = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
    for line in text.lines() {
        let t = line.trim();
        if t.starts_with('#') || t.is_empty() {
            continue;
        }
        if let Some((n, v)) = t.split_once("==") && n.trim().eq_ignore_ascii_case(name) {
            return v.trim().to_string();
        }
    }
    panic!("`{name}` is not pinned in py/requirements.txt");
}

/// What the Containerfile pins, read from the recipe the xtask actually ships.
fn image_pin(name: &str) -> String {
    let cf = facett_ops::holger::STREAMLIT_CONTAINERFILE;
    // Either an ARG (`ARG STREAMLIT_VERSION=1.60.0`) or an inline pip pin
    // (`"grpcio==1.83.0"`). Both forms are checked so the test does not depend on
    // which one the recipe happens to use for a given package.
    // The ARG form is checked FIRST. The pip line may reference the ARG
    // (`streamlit==${STREAMLIT_VERSION}`), and reading that would compare a shell
    // variable to a version number and call it a mismatch — a test failing on its
    // own parser rather than on the thing it guards.
    let arg = format!("ARG {}_VERSION=", name.to_uppercase());
    if let Some(at) = cf.find(&arg) {
        let rest = &cf[at + arg.len()..];
        let end = rest.find('\n').unwrap_or(rest.len());
        return rest[..end].trim().to_string();
    }
    let needle = format!("{name}==");
    if let Some(at) = cf.find(&needle) {
        let rest = &cf[at + needle.len()..];
        let end = rest.find(['"', ' ', '\\', '\n']).unwrap_or(rest.len());
        return rest[..end].trim().to_string();
    }
    panic!("the streamlit Containerfile pins no version for `{name}`");
}

#[test]
fn streamlit_is_pinned_to_the_verified_version() {
    assert_eq!(
        image_pin("streamlit"),
        required("streamlit"),
        "the image and py/requirements.txt disagree about the streamlit version — the \
         demo would be developed against one and run against another"
    );
}

#[test]
fn the_grpc_stack_is_pinned_to_the_verified_versions() {
    // These two are for the LIVE panes. A mismatch here surfaces as a protobuf
    // runtime error deep inside a pane, which reads as a korp-server problem.
    // `grpcio-tools` joined them on 2026-09-18: it carries protoc, which the wire
    // guards and `scripts/regen_protos.py` need, and requirements.txt had claimed
    // for months that the image installed it.
    for pkg in ["grpcio", "protobuf", "grpcio-tools"] {
        assert_eq!(
            image_pin(pkg),
            required(pkg),
            "the image and py/requirements.txt disagree about `{pkg}`"
        );
    }
}

#[test]
fn the_image_asserts_the_version_it_installed() {
    // The recipe must not merely `pip install streamlit==X` and hope. A pinned
    // install that silently resolved to something else is the same shape as the
    // `rustc --print target-list` hollow check the cargo toolchain image paid for.
    let cf = facett_ops::holger::STREAMLIT_CONTAINERFILE;
    assert!(
        cf.contains("streamlit version"),
        "the image must RUN streamlit and check the version it reports"
    );
    assert!(
        cf.contains("FATAL: asked for streamlit"),
        "the version check must fail by name, not silently"
    );
}

#[test]
fn the_wheel_under_test_is_not_baked_into_the_image() {
    // If `facett` or `facett-demo` were installed at image-build time, the running demo
    // would prove nothing about the release — it would be testing a copy that never
    // went through `publish-pip`. Both wheels arrive at RUN time (the built wheelhouse
    // or an index). Pre-0.2.0 the artifact was `facet-wrapped-web`; still banned.
    let cf = facett_ops::holger::STREAMLIT_CONTAINERFILE;
    let install_lines: Vec<&str> =
        cf.lines().filter(|l| l.contains("pip install")).collect();
    for l in &install_lines {
        assert!(
            !l.contains("facet-wrapped-web") && !l.contains("facet_wrapped_web") && !l.contains("facett"),
            "the image installs the artifact under test at BUILD time: {l}"
        );
    }
    assert!(!install_lines.is_empty(), "the image installs nothing at all?");
}

#[test]
fn the_demo_binds_loopback_only() {
    // MEASURED 2026-09-05, and it is why this test exists. The demo runs with
    // `--network host` (it must, to reach holger on 127.0.0.1:8455), which means the
    // container's interfaces ARE the host's. With streamlit's address left at
    // 0.0.0.0 it announced:
    //
    //     External URL: http://80.217.64.156:8501
    //
    // ...and the demo was reachable from the internet, on a development workstation,
    // for as long as it ran. A demo is not a service. Nothing about it is
    // authenticated, and the box it runs on holds every repo in the constellation.
    //
    // Both writers are checked: the image's default AND the command line, because
    // either one alone would silently re-open it.
    let cf = facett_ops::holger::STREAMLIT_CONTAINERFILE;
    assert!(
        !cf.contains("STREAMLIT_SERVER_ADDRESS=0.0.0.0"),
        "the streamlit image defaults to binding every interface"
    );
    assert!(
        cf.contains("STREAMLIT_SERVER_ADDRESS=127.0.0.1"),
        "the streamlit image must default to loopback"
    );

    // The launch command is the other writer, and it wins over the env default.
    let script = facett_ops::holger::streamlit_launch_command();
    assert!(
        script.contains("--server.address 127.0.0.1"),
        "the launch command must pin the bind address explicitly: {script}"
    );
    assert!(
        !script.contains("--server.address 0.0.0.0"),
        "the launch command binds every interface: {script}"
    );
}

#[test]
fn no_document_tells_anyone_to_bind_every_interface() {
    // The exposure this lane hit was not a code bug — it was streamlit's DEFAULT plus
    // `--network host`. But three documents in this repo also spelled
    // `--server.address 0.0.0.0` as the command to run, so anyone following them
    // reproduced it by hand, container or not.
    //
    // MEASURED on oden 2026-09-05: with 0.0.0.0 the demo announced
    // `External URL: http://80.217.64.156:8501` and was reachable from the internet,
    // unauthenticated, on a workstation holding every repo in the constellation.
    //
    // Guarding the CODE and leaving the instructions alone would have been the
    // narrower half of the fix: a reader follows the doc, not the Containerfile.
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
    let out = std::process::Command::new("git")
        .current_dir(root)
        .args(["ls-files"])
        .output()
        .expect("git ls-files");

    let mut hits = Vec::new();
    for rel in String::from_utf8_lossy(&out.stdout).lines() {
        // This file names the pattern in order to ban it.
        if rel.ends_with("streamlit_image_matches_requirements.rs") {
            continue;
        }
        let Ok(text) = std::fs::read_to_string(root.join(rel)) else { continue };
        for (i, line) in text.lines().enumerate() {
            if line.contains("server.address 0.0.0.0")
                || line.contains("server.address=0.0.0.0")
                || line.contains("STREAMLIT_SERVER_ADDRESS=0.0.0.0")
            {
                hits.push(format!("{rel}:{}: {}", i + 1, line.trim()));
            }
        }
    }
    assert!(
        hits.is_empty(),
        "these tell a reader to bind the demo to every interface — on a box with a \
         public IP that publishes an unauthenticated app to the internet:\n  {}",
        hits.join("\n  ")
    );
}