diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c90adab --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,76 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + name: Run Unit Tests + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests with pytest + run: | + pytest -v --cov=builders --cov-report=xml --cov-report=term + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: matrix.python-version == '3.11' + with: + file: ./coverage.xml + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + lint: + name: Code Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 black + + - name: Check code formatting with black + run: | + black --check --diff builders/ tests/ + continue-on-error: true + + - name: Lint with flake8 + run: | + # Stop the build if there are Python syntax errors or undefined names + flake8 builders/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics + # Exit-zero treats all errors as warnings + flake8 builders/ tests/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + continue-on-error: true diff --git a/requirements.txt b/requirements.txt index 760e79b..96cf364 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ PyGithub>=1.58.0 PyYAML>=5.1 +pytest>=7.4.0 +pytest-mock>=3.11.0 +pytest-cov>=4.1.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3329e93 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,39 @@ +import pytest +import os +import tempfile +import shutil + + +@pytest.fixture +def temp_repo_dir(): + """Create a temporary directory for repository simulation.""" + temp_dir = tempfile.mkdtemp() + yield temp_dir + shutil.rmtree(temp_dir, ignore_errors=True) + + +@pytest.fixture +def mock_subprocess_run(mocker): + """Mock subprocess.run to avoid actual command execution.""" + return mocker.patch('subprocess.run') + + +@pytest.fixture +def mock_os_listdir(mocker): + """Mock os.listdir for directory content simulation.""" + return mocker.patch('os.listdir') + + +@pytest.fixture +def mock_os_path_exists(mocker): + """Mock os.path.exists for file existence simulation.""" + return mocker.patch('os.path.exists') + + +@pytest.fixture +def sample_artifact(): + """Sample artifact configuration for testing.""" + return { + "version": "1.0.0", + "type": "binary" + } diff --git a/tests/test_go_binary_builder.py b/tests/test_go_binary_builder.py new file mode 100644 index 0000000..4cc571a --- /dev/null +++ b/tests/test_go_binary_builder.py @@ -0,0 +1,180 @@ +import pytest +import os +from unittest.mock import patch +from builders.binary.go_binary_builder import GoBinaryBuilder + + +class TestGoBinaryBuilder: + """Test GoBinaryBuilder build and publish methods.""" + + def test_build_success(self, temp_repo_dir, mocker): + """Test successful Go binary build.""" + # Mock subprocess.run + mock_run = mocker.patch('subprocess.run') + + # Build + builder = GoBinaryBuilder() + artifact = {"version": "1.0.0"} + result_path = builder.build(temp_repo_dir, "go-app", artifact) + + # Assertions + expected_path = os.path.join(temp_repo_dir, "build", "go-app_1.0.0_s390x") + assert os.path.normpath(result_path) == os.path.normpath(expected_path) + + # Verify Docker command was called + mock_run.assert_called_once() + docker_call = mock_run.call_args + assert "docker" in docker_call[0][0] + assert "run" in docker_call[0][0] + assert "--rm" in docker_call[0][0] + assert "ubuntu:22.04" in docker_call[0][0] + assert "go" in docker_call[0][0] + assert "build" in docker_call[0][0] + # Check that expected path appears in command (may have different separators) + assert any(os.path.normpath(expected_path) == os.path.normpath(arg) for arg in docker_call[0][0]) + + def test_build_with_custom_docker_image(self, temp_repo_dir, mocker): + """Test build with custom Docker image.""" + mock_run = mocker.patch('subprocess.run') + + builder = GoBinaryBuilder() + artifact = { + "version": "2.0.0", + "docker_image": "golang:1.21-alpine" + } + builder.build(temp_repo_dir, "custom-go-app", artifact) + + # Verify custom Docker image is used + docker_call = mock_run.call_args + assert "golang:1.21-alpine" in docker_call[0][0] + + def test_build_default_version(self, temp_repo_dir, mocker): + """Test build with default version when not specified.""" + mock_run = mocker.patch('subprocess.run') + + builder = GoBinaryBuilder() + artifact = {} # No version specified + result_path = builder.build(temp_repo_dir, "go-app", artifact) + + # Should use default version 1.0 + expected_path = os.path.join(temp_repo_dir, "build", "go-app_1.0_s390x") + assert os.path.normpath(result_path) == os.path.normpath(expected_path) + + def test_build_subprocess_error(self, temp_repo_dir, mocker): + """Test build handles subprocess errors gracefully.""" + import subprocess + mock_run = mocker.patch('subprocess.run', side_effect=subprocess.CalledProcessError( + 1, 'docker', stderr=b'Docker error' + )) + + builder = GoBinaryBuilder() + artifact = {"version": "1.0.0"} + + with pytest.raises(subprocess.CalledProcessError): + builder.build(temp_repo_dir, "error-app", artifact) + + def test_build_creates_output_directory(self, temp_repo_dir, mocker): + """Test that build creates the build directory if it doesn't exist.""" + mock_run = mocker.patch('subprocess.run') + + # Verify build directory doesn't exist initially + build_dir = os.path.join(temp_repo_dir, "build") + assert not os.path.exists(build_dir) + + builder = GoBinaryBuilder() + artifact = {"version": "1.0.0"} + builder.build(temp_repo_dir, "go-app", artifact) + + # Build directory should be created + # Note: In the actual code, this is done via os.makedirs("build", exist_ok=True) + # The mock prevents actual directory creation, so we just verify the call was made + mock_run.assert_called_once() + + @patch('lib.checksum.generate_checksum') + def test_publish_success(self, mock_checksum, temp_repo_dir, mocker): + """Test successful artifact publishing.""" + # Setup + artifact_path = os.path.join(temp_repo_dir, "go-app_1.0.0_s390x") + with open(artifact_path, "w") as f: + f.write("fake binary") + + mock_checksum.return_value = "def456" + mock_run = mocker.patch('subprocess.run') + + # Publish + builder = GoBinaryBuilder() + artifact = {"version": "1.0.0"} + builder.publish(artifact_path, "go-app", artifact) + + # Assertions + mock_checksum.assert_called_once_with(artifact_path) + mock_run.assert_called_once() + + # Verify gh release create command + gh_call = mock_run.call_args + assert "gh" in gh_call[0][0] + assert "release" in gh_call[0][0] + assert "create" in gh_call[0][0] + assert "v1.0.0" in gh_call[0][0] + assert "--title" in gh_call[0][0] + assert "Version 1.0.0" in gh_call[0][0] + assert "--generate-notes" in gh_call[0][0] + assert artifact_path in gh_call[0][0] + assert f"{artifact_path}.sha256" in gh_call[0][0] + + @patch('lib.checksum.generate_checksum') + def test_publish_default_version(self, mock_checksum, temp_repo_dir, mocker): + """Test publish with default version.""" + artifact_path = os.path.join(temp_repo_dir, "go-app_1.0_s390x") + with open(artifact_path, "w") as f: + f.write("fake binary") + + mock_checksum.return_value = "abc123" + mock_run = mocker.patch('subprocess.run') + + builder = GoBinaryBuilder() + artifact = {} # No version + builder.publish(artifact_path, "go-app", artifact) + + # Should use default version 1.0 + gh_call = mock_run.call_args + assert "v1.0" in gh_call[0][0] + + @patch('lib.checksum.generate_checksum') + def test_publish_subprocess_error(self, mock_checksum, temp_repo_dir, mocker): + """Test publish handles subprocess errors gracefully.""" + artifact_path = os.path.join(temp_repo_dir, "go-app_1.0.0_s390x") + with open(artifact_path, "w") as f: + f.write("fake binary") + + mock_checksum.return_value = "abc123" + + import subprocess + mock_run = mocker.patch('subprocess.run', side_effect=subprocess.CalledProcessError( + 1, 'gh', stderr=b'GitHub error' + )) + + builder = GoBinaryBuilder() + artifact = {"version": "1.0.0"} + + with pytest.raises(subprocess.CalledProcessError): + builder.publish(artifact_path, "go-app", artifact) + + @patch('lib.checksum.generate_checksum') + def test_publish_working_directory(self, mock_checksum, temp_repo_dir, mocker): + """Test that publish runs in the correct working directory.""" + artifact_path = os.path.join(temp_repo_dir, "build", "go-app_1.0.0_s390x") + os.makedirs(os.path.dirname(artifact_path), exist_ok=True) + with open(artifact_path, "w") as f: + f.write("fake binary") + + mock_checksum.return_value = "xyz789" + mock_run = mocker.patch('subprocess.run') + + builder = GoBinaryBuilder() + artifact = {"version": "1.0.0"} + builder.publish(artifact_path, "go-app", artifact) + + # Verify cwd parameter + gh_call = mock_run.call_args + assert gh_call[1]['cwd'] == os.path.dirname(artifact_path) diff --git a/tests/test_java_binary_builder.py b/tests/test_java_binary_builder.py new file mode 100644 index 0000000..18e54b0 --- /dev/null +++ b/tests/test_java_binary_builder.py @@ -0,0 +1,271 @@ +import pytest +import os +from unittest.mock import MagicMock, call, patch +from builders.binary.java_binary_builder import JavaBinaryBuilder, detect_build_system, BUILD_SYSTEMS + + +class TestDetectBuildSystem: + """Test build system detection for Maven and Gradle.""" + + def test_detect_maven(self, temp_repo_dir): + """Test detection of Maven build system.""" + pom_path = os.path.join(temp_repo_dir, "pom.xml") + with open(pom_path, "w") as f: + f.write("") + + system, config = detect_build_system(temp_repo_dir) + + assert system == "maven" + assert config == BUILD_SYSTEMS["maven"] + + def test_detect_gradle_groovy(self, temp_repo_dir): + """Test detection of Gradle (Groovy) build system.""" + gradle_path = os.path.join(temp_repo_dir, "build.gradle") + with open(gradle_path, "w") as f: + f.write("plugins { id 'java' }") + + system, config = detect_build_system(temp_repo_dir) + + assert system == "gradle" + assert config == BUILD_SYSTEMS["gradle"] + + def test_detect_gradle_kotlin(self, temp_repo_dir): + """Test detection of Gradle (Kotlin) build system.""" + gradle_kts_path = os.path.join(temp_repo_dir, "build.gradle.kts") + with open(gradle_kts_path, "w") as f: + f.write("plugins { kotlin(\"jvm\") }") + + system, config = detect_build_system(temp_repo_dir) + + assert system == "gradle" + assert config == BUILD_SYSTEMS["gradle"] + + def test_no_build_system_found(self, temp_repo_dir): + """Test when no build system is detected.""" + system, config = detect_build_system(temp_repo_dir) + + assert system is None + assert config is None + + +class TestJavaBinaryBuilder: + """Test JavaBinaryBuilder build and publish methods.""" + + def test_build_maven_success(self, temp_repo_dir, mocker): + """Test successful Maven build.""" + # Setup + pom_path = os.path.join(temp_repo_dir, "pom.xml") + with open(pom_path, "w") as f: + f.write("") + + target_dir = os.path.join(temp_repo_dir, "target") + os.makedirs(target_dir, exist_ok=True) + + jar_file = os.path.join(target_dir, "app-1.0.0.jar") + with open(jar_file, "w") as f: + f.write("fake jar content") + + # Mock subprocess.run + mock_run = mocker.patch('subprocess.run') + + # Mock os.listdir to return JAR files + mocker.patch('os.listdir', return_value=["app-1.0.0.jar"]) + + # Build + builder = JavaBinaryBuilder() + artifact = {"version": "1.0.0"} + result_path = builder.build(temp_repo_dir, "test-app", artifact) + + # Assertions + assert result_path == os.path.join(temp_repo_dir, "build", "test-app_1.0.0_s390x.jar") + assert mock_run.call_count == 2 # docker run + cp + + # Verify Docker command + docker_call = mock_run.call_args_list[0] + assert "docker" in docker_call[0][0] + assert "run" in docker_call[0][0] + assert "maven:3.9-eclipse-temurin-17" in docker_call[0][0] + assert "mvn" in docker_call[0][0] + assert "-DskipTests" in docker_call[0][0] + + def test_build_gradle_success(self, temp_repo_dir, mocker): + """Test successful Gradle build.""" + # Setup + gradle_path = os.path.join(temp_repo_dir, "build.gradle") + with open(gradle_path, "w") as f: + f.write("plugins { id 'java' }") + + build_dir = os.path.join(temp_repo_dir, "build", "libs") + os.makedirs(build_dir, exist_ok=True) + + jar_file = os.path.join(build_dir, "app-1.0.0.jar") + with open(jar_file, "w") as f: + f.write("fake jar content") + + # Mock subprocess.run + mock_run = mocker.patch('subprocess.run') + + # Mock os.listdir + mocker.patch('os.listdir', return_value=["app-1.0.0.jar"]) + + # Build + builder = JavaBinaryBuilder() + artifact = {"version": "2.0.0"} + result_path = builder.build(temp_repo_dir, "gradle-app", artifact) + + # Assertions + assert result_path == os.path.join(temp_repo_dir, "build", "gradle-app_2.0.0_s390x.jar") + assert mock_run.call_count == 2 + + # Verify Docker command uses Gradle + docker_call = mock_run.call_args_list[0] + assert "gradle:8.7-jdk17" in docker_call[0][0] + assert "gradle" in docker_call[0][0] + + def test_build_custom_docker_image(self, temp_repo_dir, mocker): + """Test build with custom Docker image.""" + # Setup + pom_path = os.path.join(temp_repo_dir, "pom.xml") + with open(pom_path, "w") as f: + f.write("") + + target_dir = os.path.join(temp_repo_dir, "target") + os.makedirs(target_dir, exist_ok=True) + jar_file = os.path.join(target_dir, "app.jar") + with open(jar_file, "w") as f: + f.write("fake jar") + + mock_run = mocker.patch('subprocess.run') + mocker.patch('os.listdir', return_value=["app.jar"]) + + # Build with custom image + builder = JavaBinaryBuilder() + artifact = { + "version": "1.0.0", + "docker_image": "maven:3.9-eclipse-temurin-21" + } + builder.build(temp_repo_dir, "custom-app", artifact) + + # Verify custom Docker image is used + docker_call = mock_run.call_args_list[0] + assert "maven:3.9-eclipse-temurin-21" in docker_call[0][0] + + def test_build_no_build_system_found(self, temp_repo_dir): + """Test build fails when no build system is found.""" + builder = JavaBinaryBuilder() + artifact = {"version": "1.0.0"} + + with pytest.raises(RuntimeError, match="No supported Java build file found"): + builder.build(temp_repo_dir, "no-build", artifact) + + def test_build_no_jar_found(self, temp_repo_dir, mocker): + """Test build fails when no JAR file is produced.""" + # Setup Maven project without JAR output + pom_path = os.path.join(temp_repo_dir, "pom.xml") + with open(pom_path, "w") as f: + f.write("") + + target_dir = os.path.join(temp_repo_dir, "target") + os.makedirs(target_dir, exist_ok=True) + + mock_run = mocker.patch('subprocess.run') + mocker.patch('os.listdir', return_value=[]) # No JARs + + builder = JavaBinaryBuilder() + artifact = {"version": "1.0.0"} + + with pytest.raises(RuntimeError, match="No runnable JAR found"): + builder.build(temp_repo_dir, "no-jar", artifact) + + def test_build_filters_sources_and_javadoc_jars(self, temp_repo_dir, mocker): + """Test that sources and javadoc JARs are filtered out.""" + pom_path = os.path.join(temp_repo_dir, "pom.xml") + with open(pom_path, "w") as f: + f.write("") + + target_dir = os.path.join(temp_repo_dir, "target") + os.makedirs(target_dir, exist_ok=True) + + # Create multiple JARs + for jar_name in ["app-1.0.0.jar", "app-1.0.0-sources.jar", "app-1.0.0-javadoc.jar"]: + with open(os.path.join(target_dir, jar_name), "w") as f: + f.write("fake jar") + + mock_run = mocker.patch('subprocess.run') + mocker.patch('os.listdir', return_value=[ + "app-1.0.0.jar", + "app-1.0.0-sources.jar", + "app-1.0.0-javadoc.jar" + ]) + + builder = JavaBinaryBuilder() + artifact = {"version": "1.0.0"} + builder.build(temp_repo_dir, "multi-jar", artifact) + + # Verify cp command uses main JAR, not sources/javadoc + cp_call = mock_run.call_args_list[1] + assert "app-1.0.0.jar" in cp_call[0][0][1] + assert "sources" not in cp_call[0][0][1] + assert "javadoc" not in cp_call[0][0][1] + + def test_build_subprocess_error(self, temp_repo_dir, mocker): + """Test build handles subprocess errors gracefully.""" + pom_path = os.path.join(temp_repo_dir, "pom.xml") + with open(pom_path, "w") as f: + f.write("") + + # Mock subprocess to raise error + import subprocess + mock_run = mocker.patch('subprocess.run', side_effect=subprocess.CalledProcessError(1, 'docker')) + + builder = JavaBinaryBuilder() + artifact = {"version": "1.0.0"} + + with pytest.raises(subprocess.CalledProcessError): + builder.build(temp_repo_dir, "error-app", artifact) + + @patch('lib.checksum.generate_checksum') + def test_publish_success(self, mock_checksum, temp_repo_dir, mocker): + """Test successful artifact publishing.""" + # Setup + artifact_path = os.path.join(temp_repo_dir, "test-app_1.0.0_s390x.jar") + with open(artifact_path, "w") as f: + f.write("fake jar") + + mock_checksum.return_value = "abc123" + mock_run = mocker.patch('subprocess.run') + + # Publish + builder = JavaBinaryBuilder() + artifact = {"version": "1.0.0"} + builder.publish(artifact_path, "test-app", artifact) + + # Assertions + mock_checksum.assert_called_once_with(artifact_path) + mock_run.assert_called_once() + + # Verify gh release create command + gh_call = mock_run.call_args + assert "gh" in gh_call[0][0] + assert "release" in gh_call[0][0] + assert "create" in gh_call[0][0] + assert "v1.0.0" in gh_call[0][0] + + @patch('lib.checksum.generate_checksum') + def test_publish_subprocess_error(self, mock_checksum, temp_repo_dir, mocker): + """Test publish handles errors gracefully.""" + artifact_path = os.path.join(temp_repo_dir, "test-app_1.0.0_s390x.jar") + with open(artifact_path, "w") as f: + f.write("fake jar") + + mock_checksum.return_value = "abc123" + + # Mock subprocess to raise error + import subprocess + mock_run = mocker.patch('subprocess.run', side_effect=subprocess.CalledProcessError(1, 'gh')) + + builder = JavaBinaryBuilder() + artifact = {"version": "1.0.0"} + + with pytest.raises(subprocess.CalledProcessError): + builder.publish(artifact_path, "test-app", artifact) diff --git a/tests/test_script_builder.py b/tests/test_script_builder.py new file mode 100644 index 0000000..fc9523b --- /dev/null +++ b/tests/test_script_builder.py @@ -0,0 +1,329 @@ +import pytest +import os +from unittest.mock import patch, MagicMock +from builders.script.loz_script_builder import ScriptBuilder + + +class TestScriptBuilder: + """Test ScriptBuilder build and publish methods.""" + + def test_build_success(self, temp_repo_dir, mocker): + """Test successful script-based build.""" + # Setup script repository + script_repo_path = os.path.join(temp_repo_dir, "scripts") + os.makedirs(script_repo_path, exist_ok=True) + + script_path = os.path.join(temp_repo_dir, "build.sh") + with open(script_path, "w") as f: + f.write("#!/bin/bash\necho 'Building...'") + + # Create expected output file + output_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.tar.gz") + with open(output_path, "w") as f: + f.write("fake tarball") + + # Mock subprocess.run + mock_run = mocker.patch('subprocess.run') + + # Build + builder = ScriptBuilder() + builder.set_script_repo_paths({"linux-on-ibm-z-scripts": script_repo_path}) + + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "linux-on-ibm-z-scripts", + "path": "build.sh", + "args": "--platform s390x" + } + } + result_path = builder.build(temp_repo_dir, "test-app", artifact) + + # Assertions + assert os.path.normpath(result_path) == os.path.normpath(output_path) + mock_run.assert_called_once() + + # Verify Docker command + docker_call = mock_run.call_args + assert "docker" in docker_call[0][0] + assert "run" in docker_call[0][0] + assert "bash" in docker_call[0][0] + + def test_build_with_docker_required(self, temp_repo_dir, mocker): + """Test build when Docker is required within container.""" + script_repo_path = os.path.join(temp_repo_dir, "scripts") + os.makedirs(script_repo_path, exist_ok=True) + + script_path = os.path.join(temp_repo_dir, "build.sh") + with open(script_path, "w") as f: + f.write("#!/bin/bash\necho 'Building with Docker...'") + + output_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.tar.gz") + with open(output_path, "w") as f: + f.write("fake tarball") + + # Set environment variables + mocker.patch.dict(os.environ, { + 'DOCKER_USERNAME': 'testuser', + 'DOCKER_PASSWORD': 'testpass', + 'GH_TOKEN': 'ghtoken', + 'GH_PUSH_USER': 'pushuser' + }) + + mock_run = mocker.patch('subprocess.run') + + builder = ScriptBuilder() + builder.set_script_repo_paths({"linux-on-ibm-z-scripts": script_repo_path}) + + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "linux-on-ibm-z-scripts", + "path": "build.sh", + "docker_required": True + } + } + builder.build(temp_repo_dir, "test-app", artifact) + + # Verify Docker socket is mounted + docker_call = mock_run.call_args + assert "/var/run/docker.sock:/var/run/docker.sock" in docker_call[0][0] + assert "-e" in docker_call[0][0] + + def test_build_missing_script_path(self, temp_repo_dir): + """Test build fails when script path is not specified.""" + builder = ScriptBuilder() + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "linux-on-ibm-z-scripts" + # Missing path + } + } + + with pytest.raises(ValueError, match="build_script.path required"): + builder.build(temp_repo_dir, "test-app", artifact) + + def test_build_script_repo_not_found(self, temp_repo_dir): + """Test build fails when script repository is not found.""" + builder = ScriptBuilder() + # Don't set script_repo_paths + + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "missing-repo", + "path": "build.sh" + } + } + + with pytest.raises(ValueError, match="Script repository missing-repo not found"): + builder.build(temp_repo_dir, "test-app", artifact) + + def test_build_script_file_not_found(self, temp_repo_dir): + """Test build fails when script file doesn't exist.""" + script_repo_path = os.path.join(temp_repo_dir, "scripts") + os.makedirs(script_repo_path, exist_ok=True) + + builder = ScriptBuilder() + builder.set_script_repo_paths({"linux-on-ibm-z-scripts": script_repo_path}) + + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "linux-on-ibm-z-scripts", + "path": "nonexistent.sh" + } + } + + with pytest.raises(FileNotFoundError, match="Script .* not found"): + builder.build(temp_repo_dir, "test-app", artifact) + + def test_build_output_not_created(self, temp_repo_dir, mocker): + """Test build fails when expected output is not created.""" + script_repo_path = os.path.join(temp_repo_dir, "scripts") + os.makedirs(script_repo_path, exist_ok=True) + + script_path = os.path.join(temp_repo_dir, "build.sh") + with open(script_path, "w") as f: + f.write("#!/bin/bash\necho 'Building...'") + + # Don't create output file + mock_run = mocker.patch('subprocess.run') + + builder = ScriptBuilder() + builder.set_script_repo_paths({"linux-on-ibm-z-scripts": script_repo_path}) + + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "linux-on-ibm-z-scripts", + "path": "build.sh" + } + } + + with pytest.raises(FileNotFoundError, match="Expected output .* not found"): + builder.build(temp_repo_dir, "test-app", artifact) + + def test_build_subprocess_error(self, temp_repo_dir, mocker): + """Test build handles subprocess errors gracefully.""" + script_repo_path = os.path.join(temp_repo_dir, "scripts") + os.makedirs(script_repo_path, exist_ok=True) + + script_path = os.path.join(temp_repo_dir, "build.sh") + with open(script_path, "w") as f: + f.write("#!/bin/bash\nexit 1") + + import subprocess + mock_run = mocker.patch('subprocess.run', side_effect=subprocess.CalledProcessError( + 1, 'bash', stderr=b'Script error' + )) + + builder = ScriptBuilder() + builder.set_script_repo_paths({"linux-on-ibm-z-scripts": script_repo_path}) + + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "linux-on-ibm-z-scripts", + "path": "build.sh" + } + } + + with pytest.raises(subprocess.CalledProcessError): + builder.build(temp_repo_dir, "test-app", artifact) + + def test_build_custom_docker_image(self, temp_repo_dir, mocker): + """Test build with custom Docker image.""" + script_repo_path = os.path.join(temp_repo_dir, "scripts") + os.makedirs(script_repo_path, exist_ok=True) + + script_path = os.path.join(temp_repo_dir, "build.sh") + with open(script_path, "w") as f: + f.write("#!/bin/bash\necho 'Building...'") + + output_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.tar.gz") + with open(output_path, "w") as f: + f.write("fake tarball") + + mock_run = mocker.patch('subprocess.run') + + builder = ScriptBuilder() + builder.set_script_repo_paths({"linux-on-ibm-z-scripts": script_repo_path}) + + artifact = { + "version": "1.0.0", + "build_script": { + "repo_name": "linux-on-ibm-z-scripts", + "path": "build.sh", + "docker_image": "alpine:latest" + } + } + builder.build(temp_repo_dir, "test-app", artifact) + + # Verify custom Docker image + docker_call = mock_run.call_args + assert "alpine:latest" in docker_call[0][0] + + @patch('lib.checksum.generate_checksum') + def test_publish_success(self, mock_checksum, temp_repo_dir, mocker): + """Test successful artifact publishing.""" + artifact_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.tar.gz") + with open(artifact_path, "w") as f: + f.write("fake tarball") + + mock_checksum.return_value = "abc123" + mock_run = mocker.patch('subprocess.run') + + builder = ScriptBuilder() + artifact = {"version": "1.0.0"} + builder.publish(artifact_path, "test-app", artifact) + + # Assertions + mock_checksum.assert_called_once_with(artifact_path) + mock_run.assert_called_once() + + # Verify gh release create command + gh_call = mock_run.call_args + assert "gh" in gh_call[0][0] + assert "release" in gh_call[0][0] + assert "create" in gh_call[0][0] + assert "v1.0.0" in gh_call[0][0] + + @patch('lib.checksum.generate_checksum') + def test_publish_with_rpm(self, mock_checksum, temp_repo_dir, mocker): + """Test publishing with additional RPM artifact.""" + artifact_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.tar.gz") + rpm_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.rpm") + + with open(artifact_path, "w") as f: + f.write("fake tarball") + with open(rpm_path, "w") as f: + f.write("fake rpm") + + mock_checksum.return_value = "abc123" + mock_run = mocker.patch('subprocess.run') + + builder = ScriptBuilder() + artifact = {"version": "1.0.0"} + builder.publish(artifact_path, "test-app", artifact) + + # Should call gh release twice: create + upload + assert mock_run.call_count == 2 + + # Verify upload call + upload_call = mock_run.call_args_list[1] + assert "upload" in upload_call[0][0] + # Normalize paths for cross-platform comparison + uploaded_file = os.path.normpath(upload_call[0][0][-1]) + assert os.path.normpath(rpm_path) == uploaded_file + + @patch('lib.checksum.generate_checksum') + def test_publish_with_deb(self, mock_checksum, temp_repo_dir, mocker): + """Test publishing with additional DEB artifact.""" + artifact_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.tar.gz") + deb_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.deb") + + with open(artifact_path, "w") as f: + f.write("fake tarball") + with open(deb_path, "w") as f: + f.write("fake deb") + + mock_checksum.return_value = "abc123" + mock_run = mocker.patch('subprocess.run') + + builder = ScriptBuilder() + artifact = {"version": "1.0.0"} + builder.publish(artifact_path, "test-app", artifact) + + # Should call gh release twice: create + upload + assert mock_run.call_count == 2 + + # Verify upload call + upload_call = mock_run.call_args_list[1] + assert "upload" in upload_call[0][0] + # Normalize paths for cross-platform comparison + uploaded_file = os.path.normpath(upload_call[0][0][-1]) + assert os.path.normpath(deb_path) == uploaded_file + + @patch('lib.checksum.generate_checksum') + def test_publish_subprocess_error(self, mock_checksum, temp_repo_dir, mocker): + """Test publish handles subprocess errors gracefully.""" + artifact_path = os.path.join(temp_repo_dir, "test-app-1.0.0-linux-s390x.tar.gz") + with open(artifact_path, "w") as f: + f.write("fake tarball") + + mock_checksum.return_value = "abc123" + + import subprocess + mock_run = mocker.patch('subprocess.run', side_effect=subprocess.CalledProcessError( + 1, 'gh', stderr=b'GitHub error' + )) + + builder = ScriptBuilder() + artifact = {"version": "1.0.0"} + + # Note: The actual code doesn't have error handling for publish in ScriptBuilder + # This test documents that behavior + with pytest.raises(subprocess.CalledProcessError): + builder.publish(artifact_path, "test-app", artifact)