Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions bin/base-demo-services
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,35 @@ def process_is_running(pid: int | None) -> bool:
return True


def compose_state_service_name(service: dict[str, Any]) -> str | None:
service_name = service.get("compose_service")
if isinstance(service_name, str) and service_name:
return service_name
check = service.get("check") or {}
check_service_name = check.get("service")
if check.get("type") == "compose" and isinstance(check_service_name, str) and check_service_name:
return check_service_name
return None


def compose_service_has_state(service: dict[str, Any], root: Path) -> bool:
service_name = compose_state_service_name(service)
if service_name is None or shutil.which("docker") is None:
return False
command = compose_command(root, ["ps", "--services", "--all"])
result = subprocess.run(command, check=False, capture_output=True, text=True)
if result.returncode != 0:
return False
return service_name in set(result.stdout.splitlines())


def service_has_start_state(service: dict[str, Any], root: Path, process_state: dict[str, Any] | None) -> bool:
lifecycle = service.get("lifecycle") or {}
if lifecycle.get("type") == "process":
return process_state is not None
return compose_service_has_state(service, root)


def compose_command(root: Path, args: list[str]) -> list[str]:
return [
"docker",
Expand Down Expand Up @@ -287,12 +316,17 @@ def health_label(service: dict[str, Any]) -> str:
def service_rows(services: list[dict[str, Any]], root: Path) -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
for service in services:
process_state = read_process_state(root, service)
ok, detail = check_service(service, root)
state = "healthy" if ok else "unhealthy"
if not ok and not service.get("required", False):
if ok:
state = "healthy"
elif service.get("required", False):
state = "unhealthy"
elif service_has_start_state(service, root, process_state):
state = "error"
else:
state = "stopped"
since = "-"
process_state = read_process_state(root, service)
if process_state:
started_at = process_state.get("started_at")
if isinstance(started_at, str) and started_at:
Expand Down Expand Up @@ -344,6 +378,10 @@ def cmd_check(services: list[dict[str, Any]], root: Path) -> int:
if row["state"] == "healthy":
print(f"{row['name']} ok")
continue
if row["state"] == "error":
print(f"{row['name']} error {row['detail']}")
failed = True
continue
if row["required"] == "true":
print(f"{row['name']} fail {row['detail']}")
failed = True
Expand Down
24 changes: 23 additions & 1 deletion tests/infra_test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

setup() {
TEST_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd -P)"
TEST_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/base-demo-infra-test.XXXXXX")"
}

teardown() {
rm -rf "$TEST_TMPDIR"
}

write_fake_docker_without_compose_state() {
local bin_dir="$1"
mkdir -p "$bin_dir"
cat > "$bin_dir/docker" <<'EOF'
#!/usr/bin/env bash
if [[ "$*" == *"ps --services --status running"* ]]; then
exit 0
fi
if [[ "$*" == *"ps --services --all"* ]]; then
exit 0
fi
exit 0
EOF
chmod +x "$bin_dir/docker"
}

@test "compose infrastructure files and catalog entries are present" {
Expand Down Expand Up @@ -39,7 +60,8 @@ setup() {
}

@test "services check does not require optional local infrastructure to be running" {
run "$TEST_ROOT/bin/base-demo-services" check
write_fake_docker_without_compose_state "$TEST_TMPDIR/bin"
run env BASE_DEMO_SERVICES_STATE_DIR="$TEST_TMPDIR/state" PATH="$TEST_TMPDIR/bin:$PATH" "$TEST_ROOT/bin/base-demo-services" check

[ "$status" -eq 0 ]
[[ "$output" == *"project-baseline ok"* ]]
Expand Down
24 changes: 23 additions & 1 deletion tests/native_services_test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

setup() {
TEST_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd -P)"
TEST_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/base-demo-native-services-test.XXXXXX")"
}

teardown() {
rm -rf "$TEST_TMPDIR"
}

write_fake_docker_without_compose_state() {
local bin_dir="$1"
mkdir -p "$bin_dir"
cat > "$bin_dir/docker" <<'EOF'
#!/usr/bin/env bash
if [[ "$*" == *"ps --services --status running"* ]]; then
exit 0
fi
if [[ "$*" == *"ps --services --all"* ]]; then
exit 0
fi
exit 0
EOF
chmod +x "$bin_dir/docker"
}

@test "native service files and catalog entries are present" {
Expand Down Expand Up @@ -58,7 +79,8 @@ setup() {
run "$TEST_ROOT/services/cpp-service/test.sh"
[ "$status" -eq 0 ]

run "$TEST_ROOT/bin/base-demo-services" check
write_fake_docker_without_compose_state "$TEST_TMPDIR/bin"
run env BASE_DEMO_SERVICES_STATE_DIR="$TEST_TMPDIR/state" PATH="$TEST_TMPDIR/bin:$PATH" "$TEST_ROOT/bin/base-demo-services" check
[ "$status" -eq 0 ]
[[ "$output" == *"c-service ok"* ]]
[[ "$output" == *"cpp-service ok"* ]]
Expand Down
139 changes: 138 additions & 1 deletion tests/services_test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,80 @@ teardown() {
rm -rf "$TEST_TMPDIR"
}

write_optional_file_catalog() {
local catalog="$1"

cat > "$catalog" <<EOF
{
"services": [
{
"name": "optional-file",
"kind": "service",
"runtime": "test",
"port": null,
"health_url": null,
"required": false,
"lifecycle": {
"type": "process",
"command": [
"python3",
"-c",
"import time; time.sleep(60)"
]
},
"check": {
"type": "file",
"path": "missing.optional"
},
"logs": "var/services/optional-file.log"
}
]
}
EOF
}

write_optional_compose_catalog() {
local catalog="$1"

cat > "$catalog" <<EOF
{
"services": [
{
"name": "optional-compose",
"kind": "service",
"runtime": "compose",
"port": null,
"health_url": null,
"required": false,
"compose_service": "optional-compose",
"check": {
"type": "compose",
"service": "optional-compose"
},
"logs": "docker compose logs optional-compose"
}
]
}
EOF
}

write_fake_docker_with_stopped_compose_service() {
local bin_dir="$1"
mkdir -p "$bin_dir"
cat > "$bin_dir/docker" <<'EOF'
#!/usr/bin/env bash
if [[ "$*" == *"ps --services --status running"* ]]; then
exit 0
fi
if [[ "$*" == *"ps --services --all"* ]]; then
printf 'optional-compose\n'
exit 0
fi
exit 0
EOF
chmod +x "$bin_dir/docker"
}

@test "services command is declared and executable" {
grep -Fq "services: ./bin/base-demo-services" "$TEST_ROOT/base_manifest.yaml"
[ -x "$TEST_ROOT/bin/base-demo-services" ]
Expand All @@ -27,7 +101,7 @@ teardown() {
}

@test "services check passes for healthy required entries" {
run "$TEST_ROOT/bin/base-demo-services" check
run env BASE_DEMO_SERVICES_STATE_DIR="$TEST_TMPDIR/state" "$TEST_ROOT/bin/base-demo-services" check

[ "$status" -eq 0 ]
[[ "$output" == *"project-baseline ok"* ]]
Expand Down Expand Up @@ -62,3 +136,66 @@ EOF
[[ "$output" == *"missing-required fail"* ]]
[[ "$output" == *"file:missing.file"* ]]
}

@test "services status keeps never-started optional services stopped" {
local catalog="$TEST_TMPDIR/catalog.json"
write_optional_file_catalog "$catalog"

run env BASE_DEMO_SERVICES_STATE_DIR="$TEST_TMPDIR/state" "$TEST_ROOT/bin/base-demo-services" --catalog "$catalog" status

[ "$status" -eq 0 ]
[[ "$output" == *"optional-file"*"stopped"* ]]
[[ "$output" != *"optional-file"*"error"* ]]
}

@test "services status marks started optional services with failing checks as error" {
local catalog="$TEST_TMPDIR/catalog.json"
local state_dir="$TEST_TMPDIR/state"
write_optional_file_catalog "$catalog"
mkdir -p "$state_dir"
cat > "$state_dir/optional-file.json" <<EOF
{
"pid": 999999,
"started_at": "2026-06-20T12:00:00+00:00",
"command": ["python3", "-c", "import time; time.sleep(60)"],
"log": "$state_dir/optional-file.log"
}
EOF

run env BASE_DEMO_SERVICES_STATE_DIR="$state_dir" "$TEST_ROOT/bin/base-demo-services" --catalog "$catalog" status

[ "$status" -eq 0 ]
[[ "$output" == *"optional-file"*"error"*"2026-06-20T12:00:00+00:00"* ]]
}

@test "services check reports started optional service errors" {
local catalog="$TEST_TMPDIR/catalog.json"
local state_dir="$TEST_TMPDIR/state"
write_optional_file_catalog "$catalog"
mkdir -p "$state_dir"
cat > "$state_dir/optional-file.json" <<EOF
{
"pid": 999999,
"started_at": "2026-06-20T12:00:00+00:00",
"command": ["python3", "-c", "import time; time.sleep(60)"],
"log": "$state_dir/optional-file.log"
}
EOF

run env BASE_DEMO_SERVICES_STATE_DIR="$state_dir" "$TEST_ROOT/bin/base-demo-services" --catalog "$catalog" check

[ "$status" -eq 1 ]
[[ "$output" == *"optional-file error file:missing.optional"* ]]
}

@test "services status marks optional compose services with existing state as error" {
local catalog="$TEST_TMPDIR/catalog.json"
local fake_bin="$TEST_TMPDIR/bin"
write_optional_compose_catalog "$catalog"
write_fake_docker_with_stopped_compose_service "$fake_bin"

run env PATH="$fake_bin:$PATH" "$TEST_ROOT/bin/base-demo-services" --catalog "$catalog" status

[ "$status" -eq 0 ]
[[ "$output" == *"optional-compose"*"error"* ]]
}
Loading