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
60 changes: 60 additions & 0 deletions apps/backend/analysis/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ class TestDiscoveryResult:
"command": "./gradlew connectedAndroidTest",
"coverage_command": None,
},
# Embedded: PlatformIO (host-side native env preferred, no hardware)
"pio_test": {
"config_files": ["platformio.ini"],
"type": "all",
"command": "pio test -e native",
"coverage_command": None,
},
# Embedded: Zephyr twister runs the suite on QEMU boards (emulation)
"twister": {
"config_files": ["west.yml", "prj.conf"],
"type": "all",
"command": "west twister -p qemu_x86 -T tests",
"coverage_command": None,
},
}


Expand Down Expand Up @@ -404,6 +418,7 @@ def discover(self, project_dir: Path) -> TestDiscoveryResult:
self._discover_dart_frameworks(project_dir, result)
self._discover_zig_frameworks(project_dir, result)
self._discover_haskell_frameworks(project_dir, result)
self._discover_embedded_frameworks(project_dir, result)

# Find test directories
result.test_directories = self._find_test_directories(project_dir)
Expand Down Expand Up @@ -1016,6 +1031,51 @@ def _discover_haskell_frameworks(
)
)

def _discover_embedded_frameworks(
self, project_dir: Path, result: TestDiscoveryResult
) -> None:
"""Discover embedded test setups that run without hardware.

Only emulation/host-side runners are emitted: a QA agent cannot
flash a physical board, so anything requiring one is left out.
"""
pio_ini = project_dir / "platformio.ini"
if pio_ini.exists():
try:
content = pio_ini.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
content = ""
# Prefer the host-side native environment when the project
# defines one; a bare `pio test` would target hardware envs
command = "pio test -e native" if "[env:native]" in content else "pio test"
result.frameworks.append(
TestFramework(
name="pio_test",
type="all",
command=command,
config_file="platformio.ini",
)
)

# Zephyr: prj.conf marks an application, west.yml a workspace;
# twister runs the test suite on emulated QEMU boards
if (project_dir / "west.yml").exists() or (
(project_dir / "prj.conf").exists()
and (project_dir / "CMakeLists.txt").exists()
):
result.frameworks.append(
TestFramework(
name="twister",
type="all",
command="west twister -p qemu_x86 -T tests",
config_file=(
"west.yml"
if (project_dir / "west.yml").exists()
else "prj.conf"
),
)
)

def _find_test_directories(self, project_dir: Path) -> list[str]:
"""Find test directories in the project."""
test_dir_patterns = [
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/project/command_registry/frameworks.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@
"catch2": {"ctest"},
# Swift frameworks
"vapor": {"vapor", "swift"},
# Embedded ecosystems (emulation-capable runners only)
"platformio": {"pio", "platformio"},
"zephyr": {"west", "twister", "qemu-system-arm", "qemu-system-riscv32"},
"esp-idf": {"idf.py"},
}


Expand Down
17 changes: 17 additions & 0 deletions apps/backend/project/framework_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def detect_all(self) -> list[str]:
self.detect_cpp_frameworks()
self.detect_elixir_frameworks()
self.detect_swift_frameworks()
self.detect_embedded_frameworks()
return self.frameworks

def detect_nodejs_frameworks(self) -> None:
Expand Down Expand Up @@ -406,3 +407,19 @@ def detect_swift_frameworks(self) -> None:

if "vapor" in content.lower():
self.frameworks.append("vapor")

def detect_embedded_frameworks(self) -> None:
"""Detect embedded ecosystems from their config files."""
if self.parser.file_exists("platformio.ini"):
self.frameworks.append("platformio")

# Zephyr: a west workspace manifest or an application (prj.conf + CMake)
if self.parser.file_exists("west.yml") or (
self.parser.file_exists("prj.conf")
and self.parser.file_exists("CMakeLists.txt")
):
self.frameworks.append("zephyr")

# ESP-IDF: sdkconfig lives next to the IDF CMake project
if self.parser.file_exists("sdkconfig", "sdkconfig.defaults"):
self.frameworks.append("esp-idf")
55 changes: 55 additions & 0 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,61 @@ def test_detect_cabal_test(self, discovery, temp_dir):
assert cabal.command == "cabal test"


class TestEmbeddedFrameworks:
"""Tests for embedded (PlatformIO/Zephyr) test detection."""

def test_platformio_with_native_env(self, discovery, temp_dir):
"""PlatformIO project with a native env prefers host-side tests."""
(temp_dir / "platformio.ini").write_text(
"[env:native]\nplatform = native\n\n[env:uno]\nplatform = atmelavr\n"
)

result = discovery.discover(temp_dir)

pio = next(f for f in result.frameworks if f.name == "pio_test")
assert pio.command == "pio test -e native"

def test_platformio_without_native_env(self, discovery, temp_dir):
"""PlatformIO project without native env falls back to pio test."""
(temp_dir / "platformio.ini").write_text("[env:uno]\nplatform = atmelavr\n")

result = discovery.discover(temp_dir)

pio = next(f for f in result.frameworks if f.name == "pio_test")
assert pio.command == "pio test"

def test_zephyr_workspace(self, discovery, temp_dir):
"""Zephyr west workspace gets a twister QEMU run."""
(temp_dir / "west.yml").write_text("manifest:\n projects: []\n")

result = discovery.discover(temp_dir)

twister = next(f for f in result.frameworks if f.name == "twister")
assert "qemu_x86" in twister.command
assert twister.config_file == "west.yml"

def test_zephyr_application(self, discovery, temp_dir):
"""Zephyr app (prj.conf + CMake) gets a twister QEMU run."""
(temp_dir / "prj.conf").write_text("CONFIG_GPIO=y\n")
(temp_dir / "CMakeLists.txt").write_text(
"find_package(Zephyr REQUIRED)\nproject(app)\n"
)

result = discovery.discover(temp_dir)

framework_names = [f.name for f in result.frameworks]
assert "twister" in framework_names

def test_prj_conf_without_cmake_not_zephyr(self, discovery, temp_dir):
"""A stray prj.conf without CMake does not trigger twister."""
(temp_dir / "prj.conf").write_text("CONFIG_GPIO=y\n")

result = discovery.discover(temp_dir)

framework_names = [f.name for f in result.frameworks]
assert "twister" not in framework_names


class TestTargetedCommands:
"""Tests for targeted (subset) test command templates."""

Expand Down
36 changes: 32 additions & 4 deletions tests/test_project_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def test_detects_multiple_languages(self, temp_dir: Path):

def test_detects_java_from_gradle_kts(self, temp_dir: Path):
"""Detects JVM/Gradle projects using Kotlin-DSL build files."""
(temp_dir / "build.gradle.kts").write_text("plugins { kotlin(\"jvm\") }")
(temp_dir / "build.gradle.kts").write_text('plugins { kotlin("jvm") }')
(temp_dir / "settings.gradle.kts").write_text('rootProject.name = "test"')

analyzer = ProjectAnalyzer(temp_dir)
Expand All @@ -136,7 +136,7 @@ def test_detects_java_from_gradle_kts(self, temp_dir: Path):

def test_kotlin_gradle_project_allows_gradle(self, temp_dir: Path):
"""Kotlin-only Gradle project gets gradle in stack commands."""
(temp_dir / "build.gradle.kts").write_text("plugins { kotlin(\"jvm\") }")
(temp_dir / "build.gradle.kts").write_text('plugins { kotlin("jvm") }')
src = temp_dir / "src" / "main" / "kotlin"
src.mkdir(parents=True)
(src / "Main.kt").write_text("fun main() {}")
Expand All @@ -152,7 +152,7 @@ def test_kotlin_gradle_project_allows_gradle(self, temp_dir: Path):
def test_detects_haskell(self, temp_dir: Path):
"""Detects Haskell projects."""
(temp_dir / "stack.yaml").write_text("resolver: lts-22.0")
(temp_dir / "Main.hs").write_text("main = putStrLn \"hello\"")
(temp_dir / "Main.hs").write_text('main = putStrLn "hello"')

analyzer = ProjectAnalyzer(temp_dir)
analyzer._detect_languages()
Expand All @@ -171,7 +171,7 @@ def test_detects_lua(self, temp_dir: Path):
def test_detects_perl(self, temp_dir: Path):
"""Detects Perl projects."""
(temp_dir / "cpanfile").write_text("requires 'Mojolicious';")
(temp_dir / "app.pl").write_text("print \"hello\\n\";")
(temp_dir / "app.pl").write_text('print "hello\\n";')

analyzer = ProjectAnalyzer(temp_dir)
analyzer._detect_languages()
Expand Down Expand Up @@ -410,6 +410,34 @@ def test_detects_vapor_from_package_swift(self, temp_dir: Path):

assert "vapor" in analyzer.profile.detected_stack.frameworks

def test_detects_platformio(self, temp_dir: Path):
"""Detects PlatformIO from platformio.ini."""
(temp_dir / "platformio.ini").write_text("[env:native]\nplatform = native")

analyzer = ProjectAnalyzer(temp_dir)
analyzer._detect_frameworks()

assert "platformio" in analyzer.profile.detected_stack.frameworks

def test_detects_zephyr(self, temp_dir: Path):
"""Detects Zephyr from prj.conf + CMakeLists.txt."""
(temp_dir / "prj.conf").write_text("CONFIG_GPIO=y")
(temp_dir / "CMakeLists.txt").write_text("find_package(Zephyr REQUIRED)")

analyzer = ProjectAnalyzer(temp_dir)
analyzer._detect_frameworks()

assert "zephyr" in analyzer.profile.detected_stack.frameworks

def test_detects_esp_idf(self, temp_dir: Path):
"""Detects ESP-IDF from sdkconfig."""
(temp_dir / "sdkconfig").write_text("CONFIG_IDF_TARGET=esp32")

analyzer = ProjectAnalyzer(temp_dir)
analyzer._detect_frameworks()

assert "esp-idf" in analyzer.profile.detected_stack.frameworks


class TestDatabaseDetection:
"""Tests for database detection."""
Expand Down
Loading