Skip to content
Open
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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1154,3 +1154,6 @@ if (GITHUB_ACTIONS_BUILD)
fsdb_all_services
)
endif()

# FBOSS Image Builder distro_cli tests
include(cmake/FbossImageDistroCliTests.cmake)
2 changes: 2 additions & 0 deletions build/fbcode_builder/CMake/FBPythonBinary.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ function(add_fb_python_executable TARGET)
# CMake doesn't really seem to like having a directory specified as an
# output; specify the __main__.py file as the output instead.
set(zipapp_output_file "${zipapp_output}/__main__.py")
# Update output_file to match zipapp_output_file for dir type
set(output_file "${zipapp_output_file}")
list(APPEND
extra_cmd_params
COMMAND "${CMAKE_COMMAND}" -E remove_directory "${zipapp_output}"
Expand Down
92 changes: 92 additions & 0 deletions cmake/FbossImageDistroCliTests.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# CMake to build and test fboss-image/distro_cli

# In general, libraries and binaries in fboss/foo/bar are built by
# cmake/FooBar.cmake

# distro_cli requires Python 3.10+ with widespread use of union type syntax
# Save and temporarily override Python3_EXECUTABLE
if(DEFINED Python3_EXECUTABLE)
set(SAVED_Python3_EXECUTABLE "${Python3_EXECUTABLE}")
endif()

# Find the highest available Python version >= 3.10
find_package(Python3 3.10 COMPONENTS Interpreter REQUIRED)
message(STATUS "Using Python ${Python3_VERSION} (${Python3_EXECUTABLE}) for distro_cli tests")

include(FBPythonBinary)

file(GLOB DISTRO_CLI_TEST_SOURCES
"fboss-image/distro_cli/tests/*_test.py"
)

# Exclude image_builder_test.py - requires source tree access for templates
# which isn't available in CMake build directory
list(FILTER DISTRO_CLI_TEST_SOURCES EXCLUDE REGEX "image_builder_test\\.py$")

# Exclude: manual e2e tests
list(FILTER DISTRO_CLI_TEST_SOURCES EXCLUDE REGEX "kernel_build_test\\.py$")
list(FILTER DISTRO_CLI_TEST_SOURCES EXCLUDE REGEX "sai_build_test\\.py$")

# Exclude: Docker not available
list(FILTER DISTRO_CLI_TEST_SOURCES EXCLUDE REGEX "build_entrypoint_test\\.py$")
list(FILTER DISTRO_CLI_TEST_SOURCES EXCLUDE REGEX "build_test\\.py$")
list(FILTER DISTRO_CLI_TEST_SOURCES EXCLUDE REGEX "docker_test\\.py$")

file(GLOB DISTRO_CLI_TEST_HELPERS
"fboss-image/distro_cli/tests/test_helpers.py"
)

file(GLOB_RECURSE DISTRO_CLI_LIB_SOURCES
"fboss-image/distro_cli/builder/*.py"
"fboss-image/distro_cli/cmds/*.py"
"fboss-image/distro_cli/lib/*.py"
"fboss-image/distro_cli/tools/*.py"
)

# Create Python unittest executable with test data files
# Use TYPE "dir" to create a directory-based executable instead of zipapp.
# This allows tests to access data files via Path(__file__).parent, which
# doesn't work inside zip archives.
#
# Note: Only include .py files in SOURCES. Non-Python data files would be
# treated as Python modules during test discovery, causing import errors.
# Data files are copied via add_custom_command below after the build
# generates the directory structure.
add_fb_python_unittest(
distro_cli_tests
BASE_DIR "fboss-image"
TYPE "dir"
SOURCES
${DISTRO_CLI_TEST_SOURCES}
${DISTRO_CLI_TEST_HELPERS}
${DISTRO_CLI_LIB_SOURCES}
ENV
"PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}/fboss-image"
)

# Copy test data files AFTER the build generates the directory structure
# Tests access these via Path(__file__).parent / "data" / "filename"
# With TYPE "dir", the executable is created at distro_cli_tests/ so data
# files need to be inside that directory structure.
#
# We use add_custom_command to copy AFTER the .GEN_PY_EXE target runs,
# because that target creates/recreates the distro_cli_tests/ directory.
set(DATA_DEST_DIR "${CMAKE_CURRENT_BINARY_DIR}/distro_cli_tests/distro_cli/tests")
set(DATA_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/fboss-image/distro_cli/tests/data")

add_custom_command(
TARGET distro_cli_tests.GEN_PY_EXE
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${DATA_SOURCE_DIR}"
"${DATA_DEST_DIR}/data"
COMMENT "Copying test data files for distro_cli_tests"
)

install_fb_python_executable(distro_cli_tests)

# Restore the original Python3_EXECUTABLE if it was set
if(DEFINED SAVED_Python3_EXECUTABLE)
set(Python3_EXECUTABLE "${SAVED_Python3_EXECUTABLE}")
unset(SAVED_Python3_EXECUTABLE)
endif()
68 changes: 64 additions & 4 deletions fboss-image/distro_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,56 @@ Components are built in the following order:

### Running Tests

#### With Bazel (Recommended)

```bash
# Run all tests
cd fboss-image/distro_cli
python3 -m unittest discover -s tests -p '*_test.py'
bazel test //fboss/fboss-image/distro_cli:all

# Run specific test
python3 -m unittest tests.cli_test
bazel test //fboss/fboss-image/distro_cli:cli_test

# Run with detailed output
bazel test //fboss/fboss-image/distro_cli:all --test_output=all
```

#### With pytest

```bash
# Run all tests (from fboss-image directory)
cd fboss-image
PYTHONPATH=. python3 -m pytest distro_cli/tests/ -v

# Run specific test file
PYTHONPATH=. python3 -m pytest distro_cli/tests/cli_test.py -v

# Run specific test class or method
PYTHONPATH=. python3 -m pytest distro_cli/tests/cli_test.py::CLITest::test_cli_creation -v
```

#### With unittest

```bash
# Run all tests (from fboss-image directory)
cd fboss-image
PYTHONPATH=. python3 -m unittest discover -s distro_cli/tests -p '*_test.py'

# Run specific test module
PYTHONPATH=. python3 -m unittest distro_cli.tests.cli_test

# Run specific test class
PYTHONPATH=. python3 -m unittest distro_cli.tests.cli_test.CLITest
```

### Linting

```bash
# With ruff (from distro_cli directory)
cd fboss-image/distro_cli
python3 -m ruff check .

# With bazel (from repository root)
bazel test //private-fboss/fboss-image/distro_cli:lint_check
```

### Project Structure
Expand Down Expand Up @@ -152,7 +188,7 @@ fboss-image/distro_cli/
The CLI uses a custom OOP wrapper around argparse (stdlib only, no external dependencies):

```python
from lib.cli import CLI
from distro_cli.lib.cli import CLI

# Create CLI
cli = CLI(description='My CLI')
Expand All @@ -171,3 +207,27 @@ device.add_command('ssh', ssh_func, help_text='SSH to device')
# Run
cli.run(setup_logging_func=setup_logging)
```

## Running Tests

### Quick Tests (Default)
Run all fast unit tests (excludes long-running E2E tests):
```bash
python3 -m pytest distro_cli/tests/ -v
# or explicitly exclude e2e tests:
python3 -m pytest distro_cli/tests/ -m "not e2e" -v
```

### E2E Tests Only
Run only the end-to-end tests (kernel build, SAI build):
```bash
python3 -m pytest distro_cli/tests/ -m e2e -v -s
```

### All Tests (Including E2E)
Run everything:
```bash
python3 -m pytest distro_cli/tests/ -v -s
```

**Note:** E2E tests can take 10-60 minutes and require actual source files (kernel sources, SAI SDK, etc.).
8 changes: 8 additions & 0 deletions fboss-image/distro_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) 2004-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

"""FBOSS Distribution CLI package."""
8 changes: 8 additions & 0 deletions fboss-image/distro_cli/cmds/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) 2004-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

"""FBOSS Distribution CLI commands package."""
8 changes: 8 additions & 0 deletions fboss-image/distro_cli/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) 2004-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

"""FBOSS Distribution CLI library package."""
11 changes: 11 additions & 0 deletions fboss-image/distro_cli/lib/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright (c) 2004-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

"""Constants for FBOSS image builder."""

# Docker image names
FBOSS_BUILDER_IMAGE = "fboss_builder"
57 changes: 57 additions & 0 deletions fboss-image/distro_cli/lib/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) 2004-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

"""Exception definitions for FBOSS image builder."""


class FbossImageError(Exception):
"""Base exception for all fboss-image errors.
All custom exceptions in the fboss-image tool should inherit from this.
This allows the top-level CLI handler to catch and format errors appropriately
before returning a non-zero exit code.
"""


class BuildError(FbossImageError):
"""Build command failed.
Raised when a component build fails, including:
- Build script/command not found
- Build process returned non-zero exit code
- Build output validation failed
"""


class ManifestError(FbossImageError):
"""Manifest parsing or validation failed.
Raised when:
- Manifest file not found or invalid JSON
- Required fields missing
- Invalid manifest structure
"""


class ArtifactError(FbossImageError):
"""Artifact operation failed.
Raised when:
- Expected artifact not found after build
- Artifact download failed
- Artifact cache operation failed
"""


class ComponentError(FbossImageError):
"""Component-specific error.
Raised when:
- Component not found in manifest
- Component configuration invalid
- Component builder not implemented
"""
64 changes: 64 additions & 0 deletions fboss-image/distro_cli/lib/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) 2004-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

"""Path resolution utilities for FBOSS image builder."""

from functools import lru_cache
from pathlib import Path


@lru_cache(maxsize=1)
def get_root_dir() -> Path:
"""Find the repository root directory.
This works by walking up from the current file until we find
the 'fboss-image' directory, then returning its parent.
Additionally verifies that both 'fboss-image' and 'fboss' directories
exist at the root to ensure we're in the correct repository structure.
The result is cached after the first call for performance.
Returns:
Path to repository root directory
Raises:
RuntimeError: If the root directory cannot be determined
"""
current = Path(__file__).resolve()

# Walk up the directory tree looking for fboss-image
for parent in current.parents:
if (parent / "fboss-image").is_dir() and (parent / "fboss").is_dir():
return parent

raise RuntimeError(
f"Could not find repository root from {current}. "
f"Expected to find 'fboss-image' and 'fboss' directories in parent path."
)


def get_abs_path(path_parts: str | list[str]) -> Path:
"""Get absolute path by joining the parent of 'fboss-image' with provided path parts.
Args:
path_parts: Either a string like "fboss-image/distro_cli/tools" or
a list like ["fboss-image", "distro_cli", "tools"]
Returns:
Absolute path
Examples:
get_abs_path("fboss-image/distro_cli/tools")
get_abs_path(["fboss-image", "distro_cli", "tools"])
get_abs_path("fboss/oss/scripts")
"""
root = get_root_dir()

if isinstance(path_parts, str):
return root / path_parts
return root.joinpath(*path_parts)
8 changes: 8 additions & 0 deletions fboss-image/distro_cli/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) 2004-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.

"""FBOSS Distribution CLI tests package."""
Loading