diff --git a/.github/configs/actionlint.yml b/.github/configs/actionlint.yml new file mode 100644 index 000000000..6f4bbed1c --- /dev/null +++ b/.github/configs/actionlint.yml @@ -0,0 +1,6 @@ +self-hosted-runner: + # Labels of self-hosted runner in array of strings. + labels: + - DPDK + - runner-1 + - runner-4 diff --git a/.github/workflows/bare-metal-build.yml b/.github/workflows/bare-metal-build.yml new file mode 100644 index 000000000..f96ffce35 --- /dev/null +++ b/.github/workflows/bare-metal-build.yml @@ -0,0 +1,189 @@ +name: Base Build + +on: + workflow_call: + inputs: + branch: + required: false + type: string + default: "main" + description: "Branch to checkout" + tag: + required: false + type: string + description: "Tag to checkout" + runner: + required: false + type: string + default: "runner-4" + description: "Runner to use for the build job" + +env: + BUILD_TYPE: Release + BUILD_DIR: "${{ github.workspace }}/_build" + DEBIAN_FRONTEND: noninteractive + MTL_BUILD_DISABLE_PCAPNG: true + PREFIX_DIR: "${{ github.workspace }}/_install" + +defaults: + run: + shell: bash + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + build-baremetal-ubuntu: + runs-on: ${{ inputs.runner }} + timeout-minutes: 120 + steps: + - name: "Harden Runner" + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: audit + - name: "Fix permissions before checkout" + run: | + if [ -d "${{ github.workspace }}" ]; then + sudo chown -R "${USER}" "${{ github.workspace }}" || true + sudo chmod -R u+w "${{ github.workspace }}" || true + fi + + - name: "Checkout repository" + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + ref: ${{ inputs.tag || inputs.branch }} + + - name: "Install OS level dependencies" + run: eval 'source scripts/setup_build_env.sh && install_package_dependencies' + + - name: "Check local dependencies build cache" + id: load-local-dependencies-cache + uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: ${{ env.BUILD_DIR }} + key: ${{ runner.os }}-${{ hashFiles('versions.env') }}-${{ hashFiles('scripts/setup*.sh') }} + + - name: "Download, unpack and patch build dependencies" + if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' + run: eval 'source scripts/setup_build_env.sh && get_download_unpack_dependencies' + + - name: "Clone and patch ffmpeg 7.0" + if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' + run: | + ffmpeg-plugin/clone-and-patch-ffmpeg.sh "7.0" + + - name: "Build and Install xdp and libbpf" + run: eval 'source scripts/setup_build_env.sh && lib_install_xdp_bpf_tools' + + - name: "Build and Install libfabric" + run: eval 'source scripts/setup_build_env.sh && lib_install_fabrics' + + - name: "Build and Install the DPDK (skipped: already installed on runner)" + if: always() && false # Always skip this step for now + run: | + echo "Skipping DPDK build and install as it is already installed on the machine." + # eval 'source scripts/setup_build_env.sh && lib_install_dpdk' + + - name: "Check if DPDK version needs to be updated" + id: check_dpdk_version + run: | + if grep -q "DPDK_VER=25.03" "${{ github.workspace }}/versions.env" && \ + grep -q "DPDK_VER=25.03" "${{ github.workspace }}/ffmpeg-plugin/versions.env" && \ + grep -q "DPDK_VER=25.03" "${{ github.workspace }}/media-proxy/versions.env" && \ + grep -q "DPDK_VER=25.03" "${{ github.workspace }}/sdk/versions.env"; then + echo "DPDK version is already set correctly." + echo "need_update=false" >> "$GITHUB_OUTPUT" + else + echo "DPDK version needs to be updated." + echo "need_update=true" >> "$GITHUB_OUTPUT" + fi + + - name: "Switch DPDK version to currently used on the machine" + if: steps.check_dpdk_version.outputs.need_update == 'true' + run: | + sed -i 's|DPDK_VER=23.11|DPDK_VER=25.03|g' \ + "${{ github.workspace }}/versions.env" \ + "${{ github.workspace }}/ffmpeg-plugin/versions.env" \ + "${{ github.workspace }}/media-proxy/versions.env" \ + "${{ github.workspace }}/sdk/versions.env" + + - name: "Check if MTL version needs to be updated" + id: check_mtl_version + run: | + if grep -q "MTL_VER=main" "${{ github.workspace }}/versions.env" && \ + grep -q "MTL_VER=main" "${{ github.workspace }}/ffmpeg-plugin/versions.env" && \ + grep -q "MTL_VER=main" "${{ github.workspace }}/media-proxy/versions.env" && \ + grep -q "MTL_VER=main" "${{ github.workspace }}/sdk/versions.env"; then + echo "MTL version is already set correctly." + echo "need_update=false" >> "$GITHUB_OUTPUT" + else + echo "MTL version needs to be updated." + echo "need_update=true" >> "$GITHUB_OUTPUT" + fi + + - name: "Switch MTL version to currently used on the machine" + if: steps.check_mtl_version.outputs.need_update == 'true' + run: | + sed -i 's|MTL_VER=v25.02|MTL_VER=main|g' \ + "${{ github.workspace }}/versions.env" \ + "${{ github.workspace }}/ffmpeg-plugin/versions.env" \ + "${{ github.workspace }}/media-proxy/versions.env" \ + "${{ github.workspace }}/sdk/versions.env" + + - name: "Build and Install the MTL(skipped: already installed on runner)" + if: always() && false # Always skip this step for now + run: | + echo "Skipping MTL build and install as it is already installed on the machine." + # eval 'source scripts/setup_build_env.sh && lib_install_mtl' + + - name: "Build and Install JPEG XS" + run: eval 'source scripts/setup_build_env.sh && lib_install_jpeg_xs' + + - name: "Build and Install JPEG XS ffmpeg plugin" + run: eval 'source scripts/setup_build_env.sh && lib_install_mtl_jpeg_xs_plugin' + + - name: "Build gRPC and dependencies" + run: eval 'source scripts/setup_build_env.sh && lib_install_grpc' + + - name: "Build MCM SDK and Media Proxy" + run: eval 'source scripts/common.sh && ./build.sh "${PREFIX_DIR}"' + + - name: "Build FFmpeg 7.0 with MCM plugin" + working-directory: ${{ github.workspace }}/ffmpeg-plugin + run: | + ./configure-ffmpeg.sh "7.0" --prefix=${{ env.BUILD_DIR }}/ffmpeg-7-0 --disable-doc --disable-debug && \ + ./build-ffmpeg.sh "7.0" + + - name: Install RxTxApp dependencies + run: sudo apt-get update && sudo apt-get install -y libjansson-dev + - name: "build RxTxApp" + working-directory: ${{ github.workspace }}/tests/tools/TestApp + run: | + rm -rf build && \ + mkdir build && cd build && \ + cmake .. && \ + make + - name: "clone FFMPEG repository" + run: | + echo "Cloning FFMPEG repository" + - name: "clone MTL repository" + run: | + echo "Cloning MTL repository" + - name: "build MTL FFMPEG" + run: | + echo "Building MTL FFMPEG" + + - name: "upload media-proxy and mcm binaries" + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: mcm-build + path: | + ${{ env.BUILD_DIR }}/mcm/bin/media_proxy + ${{ env.BUILD_DIR }}/mcm/bin/mesh-agent + ${{ env.BUILD_DIR }}/mcm/lib/libmcm_dp.so.* + ${{ env.BUILD_DIR }}/ffmpeg-7-0/ffmpeg + ${{ env.BUILD_DIR }}/ffmpeg-7-0/lib/** diff --git a/.github/workflows/base_build.yml b/.github/workflows/base_build.yml index 6abb7ee03..7cd75e120 100644 --- a/.github/workflows/base_build.yml +++ b/.github/workflows/base_build.yml @@ -2,9 +2,23 @@ name: Base Build on: push: - branches: [ "main" ] + branches: ["main"] + paths-ignore: + - "**/*.md" + - "tests/**" + - "docs/**" + - "LICENSE" + - ".gitignore" + - ".editorconfig" pull_request: - branches: [ "main" ] + branches: ["main"] + paths-ignore: + - "**/*.md" + - "tests/**" + - "docs/**" + - "LICENSE" + - ".gitignore" + - ".editorconfig" workflow_dispatch: env: @@ -27,80 +41,80 @@ concurrency: jobs: build-baremetal-ubuntu: - runs-on: 'ubuntu-22.04' + runs-on: "ubuntu-22.04" timeout-minutes: 120 steps: - - name: 'Harden Runner' - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 - with: - egress-policy: audit - - - name: 'Checkout repository' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: 'Install OS level dependencies' - run: eval 'source scripts/setup_build_env.sh && install_package_dependencies' - - - name: 'Check local dependencies build cache' - id: load-local-dependencies-cache - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: ${{ env.BUILD_DIR }} - key: ${{ runner.os }}-${{ hashFiles('versions.env') }}-${{ hashFiles('scripts/setup*.sh') }} - - - name: 'Download, unpack and patch build dependencies' - if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' - run: eval 'source scripts/setup_build_env.sh && get_download_unpack_dependencies' - - - name: 'Clone and patch ffmpeg 6.1 and 7.0' - if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' - run: | - ffmpeg-plugin/clone-and-patch-ffmpeg.sh "6.1" - ffmpeg-plugin/clone-and-patch-ffmpeg.sh "7.0" - - - name: 'Build and Install xdp and libbpf' - run: eval 'source scripts/setup_build_env.sh && lib_install_xdp_bpf_tools' - - - name: 'Build and Install libfabric' - run: eval 'source scripts/setup_build_env.sh && lib_install_fabrics' - - - name: 'Build and Install the DPDK' - run: eval 'source scripts/setup_build_env.sh && lib_install_dpdk' - - - name: 'Build and Install the MTL' - run: eval 'source scripts/setup_build_env.sh && lib_install_mtl' - - - name: 'Build and Install JPEG XS' - run: eval 'source scripts/setup_build_env.sh && lib_install_jpeg_xs' - - - name: 'Build and Install JPEG XS ffmpeg plugin' - run: eval 'source scripts/setup_build_env.sh && lib_install_mtl_jpeg_xs_plugin' - - - name: 'Build gRPC and dependencies' - run: eval 'source scripts/setup_build_env.sh && lib_install_grpc' - - - name: 'Build MCM SDK and Media Proxy' - run: eval 'source scripts/common.sh && ./build.sh "${PREFIX_DIR}"' - - - name: 'Build FFmpeg 6.1 with MCM plugin' - working-directory: ${{ github.workspace }}/ffmpeg-plugin - run: | - ./configure-ffmpeg.sh "6.1" --disable-doc --disable-debug && \ - ./build-ffmpeg.sh "6.1" - - - name: 'Build FFmpeg 7.0 with MCM plugin' - working-directory: ${{ github.workspace }}/ffmpeg-plugin - run: | - ./configure-ffmpeg.sh "7.0" --disable-doc --disable-debug && \ - ./build-ffmpeg.sh "7.0" - - - name: 'upload media-proxy and mcm binaries' - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: mcm-build - path: | - ${{ env.BUILD_DIR }}/mcm/bin/media_proxy - ${{ env.BUILD_DIR }}/mcm/bin/mesh-agent - ${{ env.BUILD_DIR }}/mcm/lib/libmcm_dp.so.* - ${{ env.BUILD_DIR }}/ffmpeg-6-1/ffmpeg - ${{ env.BUILD_DIR }}/ffmpeg-7-0/ffmpeg + - name: "Harden Runner" + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: audit + + - name: "Checkout repository" + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: "Install OS level dependencies" + run: eval 'source scripts/setup_build_env.sh && install_package_dependencies' + + - name: "Check local dependencies build cache" + id: load-local-dependencies-cache + uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: ${{ env.BUILD_DIR }} + key: ${{ runner.os }}-${{ hashFiles('versions.env') }}-${{ hashFiles('scripts/setup*.sh') }} + + - name: "Download, unpack and patch build dependencies" + if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' + run: eval 'source scripts/setup_build_env.sh && get_download_unpack_dependencies' + + - name: "Clone and patch ffmpeg 6.1 and 7.0" + if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' + run: | + ffmpeg-plugin/clone-and-patch-ffmpeg.sh "6.1" + ffmpeg-plugin/clone-and-patch-ffmpeg.sh "7.0" + + - name: "Build and Install xdp and libbpf" + run: eval 'source scripts/setup_build_env.sh && lib_install_xdp_bpf_tools' + + - name: "Build and Install libfabric" + run: eval 'source scripts/setup_build_env.sh && lib_install_fabrics' + + - name: "Build and Install the DPDK" + run: eval 'source scripts/setup_build_env.sh && lib_install_dpdk' + + - name: "Build and Install the MTL" + run: eval 'source scripts/setup_build_env.sh && lib_install_mtl' + + - name: "Build and Install JPEG XS" + run: eval 'source scripts/setup_build_env.sh && lib_install_jpeg_xs' + + - name: "Build and Install JPEG XS ffmpeg plugin" + run: eval 'source scripts/setup_build_env.sh && lib_install_mtl_jpeg_xs_plugin' + + - name: "Build gRPC and dependencies" + run: eval 'source scripts/setup_build_env.sh && lib_install_grpc' + + - name: "Build MCM SDK and Media Proxy" + run: eval 'source scripts/common.sh && ./build.sh "${PREFIX_DIR}"' + + - name: "Build FFmpeg 6.1 with MCM plugin" + working-directory: ${{ github.workspace }}/ffmpeg-plugin + run: | + ./configure-ffmpeg.sh "6.1" --disable-doc --disable-debug && \ + ./build-ffmpeg.sh "6.1" + + - name: "Build FFmpeg 7.0 with MCM plugin" + working-directory: ${{ github.workspace }}/ffmpeg-plugin + run: | + ./configure-ffmpeg.sh "7.0" --disable-doc --disable-debug && \ + ./build-ffmpeg.sh "7.0" + + - name: "upload media-proxy and mcm binaries" + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: mcm-build + path: | + ${{ env.BUILD_DIR }}/mcm/bin/media_proxy + ${{ env.BUILD_DIR }}/mcm/bin/mesh-agent + ${{ env.BUILD_DIR }}/mcm/lib/libmcm_dp.so.* + ${{ env.BUILD_DIR }}/ffmpeg-6-1/ffmpeg + ${{ env.BUILD_DIR }}/ffmpeg-7-0/ffmpeg diff --git a/.github/workflows/build_docker_tpl.yml b/.github/workflows/build_docker_tpl.yml index 2571b14dc..ae26615aa 100644 --- a/.github/workflows/build_docker_tpl.yml +++ b/.github/workflows/build_docker_tpl.yml @@ -6,15 +6,15 @@ on: build_type: required: false type: string - default: 'Release' + default: "Release" docker_registry: required: false type: string - default: 'ghcr.io' + default: "ghcr.io" docker_registry_prefix: required: false type: string - default: 'openvisualcloud/media-communications-mesh' + default: "openvisualcloud/media-communications-mesh" docker_registry_login: required: false type: boolean @@ -26,11 +26,11 @@ on: docker_build_args: required: false type: string - default: '' + default: "" docker_build_platforms: required: false type: string - default: 'linux/amd64' + default: "linux/amd64" docker_image_tag: required: false type: string @@ -40,7 +40,7 @@ on: docker_file_path: required: false type: string - default: './Dockerfile' + default: "./Dockerfile" secrets: docker_registry_login: required: false diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index 6cea7daee..3da9ee595 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -2,12 +2,12 @@ name: Coverity Build on: schedule: - - cron: '0 18 * * *' + - cron: "0 18 * * *" workflow_dispatch: inputs: branch: - description: 'Branch to run scans on' - default: 'main' + description: "Branch to run scans on" + default: "main" type: string env: @@ -26,81 +26,81 @@ concurrency: jobs: coverity: - runs-on: 'ubuntu-22.04' + runs-on: "ubuntu-22.04" timeout-minutes: 90 steps: - - name: 'Harden Runner' - uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 - with: - egress-policy: audit - - - name: 'Checkout repository' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - ref: ${{ inputs.branch }} - - - name: 'Install OS level dependencies' - run: eval 'source scripts/setup_build_env.sh && install_package_dependencies' - - - name: 'Check local dependencies build cache' - id: load-local-dependencies-cache - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: ${{ env.BUILD_DIR }} - key: ${{ runner.os }}-${{ hashFiles('versions.env') }}-${{ hashFiles('scripts/setup*.sh') }} - - - name: 'Download, unpack and patch build dependencies' - if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' - run: eval 'source scripts/setup_build_env.sh && get_download_unpack_dependencies' - - - name: 'Clone and patch ffmpeg 6.1 and 7.0' - if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' - run: | - ffmpeg-plugin/clone-and-patch-ffmpeg.sh "6.1" - ffmpeg-plugin/clone-and-patch-ffmpeg.sh "7.0" - - - name: 'Build and Install xdp and libbpf' - run: eval 'source scripts/setup_build_env.sh && lib_install_xdp_bpf_tools' - - - name: 'Build and Install libfabric' - run: eval 'source scripts/setup_build_env.sh && lib_install_fabrics' - - - name: 'Build and Install the DPDK' - run: eval 'source scripts/setup_build_env.sh && lib_install_dpdk' - - - name: 'Build and Install the MTL' - run: eval 'source scripts/setup_build_env.sh && lib_install_mtl' - - - name: 'Build and Install JPEG XS' - run: eval 'source scripts/setup_build_env.sh && lib_install_jpeg_xs' - - - name: 'Build and Install JPEG XS ffmpeg plugin' - run: eval 'source scripts/setup_build_env.sh && lib_install_mtl_jpeg_xs_plugin' - - - name: 'Build gRPC and dependencies' - run: eval 'source scripts/setup_build_env.sh && lib_install_grpc' - - - name: 'Configure ffmpeg and dependencies' - run: | - sed -i 's/strlen (MEMIF_DEFAULT_APP_NAME)/(sizeof(MEMIF_DEFAULT_APP_NAME) - 1)/g' ${{ github.workspace }}/sdk/3rdparty/libmemif/src/memif_private.h && \ - ${{ github.workspace }}/build.sh && \ - ${{ github.workspace }}/ffmpeg-plugin/configure-ffmpeg.sh "6.1" --disable-doc --disable-debug && \ - ${{ github.workspace }}/ffmpeg-plugin/configure-ffmpeg.sh "7.0" --disable-doc --disable-debug && \ - rm -rf ${{ github.workspace }}/_build/mcm - echo "\"${{ github.workspace }}/ffmpeg-plugin/build-ffmpeg.sh\" \"6.1\"" > ${{ github.workspace }}/build.sh - echo "\"${{ github.workspace }}/ffmpeg-plugin/build-ffmpeg.sh\" \"7.0\"" > ${{ github.workspace }}/build.sh - - - name: 'Run coverity' - uses: vapier/coverity-scan-action@2068473c7bdf8c2fb984a6a40ae76ee7facd7a85 # v1.8.0 - with: - project: 'Media-Communications-Mesh' - email: ${{ secrets.COVERITY_SCAN_EMAIL }} - token: ${{ secrets.COVERITY_SCAN_TOKEN }} - build_language: 'cxx' - build_platform: 'linux64' - command: ${{ github.workspace }}/build.sh - - - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: coverity-reports - path: '${{ github.workspace }}/cov-int' + - name: "Harden Runner" + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: audit + + - name: "Checkout repository" + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + ref: ${{ inputs.branch }} + + - name: "Install OS level dependencies" + run: eval 'source scripts/setup_build_env.sh && install_package_dependencies' + + - name: "Check local dependencies build cache" + id: load-local-dependencies-cache + uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: ${{ env.BUILD_DIR }} + key: ${{ runner.os }}-${{ hashFiles('versions.env') }}-${{ hashFiles('scripts/setup*.sh') }} + + - name: "Download, unpack and patch build dependencies" + if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' + run: eval 'source scripts/setup_build_env.sh && get_download_unpack_dependencies' + + - name: "Clone and patch ffmpeg 6.1 and 7.0" + if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' + run: | + ffmpeg-plugin/clone-and-patch-ffmpeg.sh "6.1" + ffmpeg-plugin/clone-and-patch-ffmpeg.sh "7.0" + + - name: "Build and Install xdp and libbpf" + run: eval 'source scripts/setup_build_env.sh && lib_install_xdp_bpf_tools' + + - name: "Build and Install libfabric" + run: eval 'source scripts/setup_build_env.sh && lib_install_fabrics' + + - name: "Build and Install the DPDK" + run: eval 'source scripts/setup_build_env.sh && lib_install_dpdk' + + - name: "Build and Install the MTL" + run: eval 'source scripts/setup_build_env.sh && lib_install_mtl' + + - name: "Build and Install JPEG XS" + run: eval 'source scripts/setup_build_env.sh && lib_install_jpeg_xs' + + - name: "Build and Install JPEG XS ffmpeg plugin" + run: eval 'source scripts/setup_build_env.sh && lib_install_mtl_jpeg_xs_plugin' + + - name: "Build gRPC and dependencies" + run: eval 'source scripts/setup_build_env.sh && lib_install_grpc' + + - name: "Configure ffmpeg and dependencies" + run: | + sed -i 's/strlen (MEMIF_DEFAULT_APP_NAME)/(sizeof(MEMIF_DEFAULT_APP_NAME) - 1)/g' ${{ github.workspace }}/sdk/3rdparty/libmemif/src/memif_private.h && \ + ${{ github.workspace }}/build.sh && \ + ${{ github.workspace }}/ffmpeg-plugin/configure-ffmpeg.sh "6.1" --disable-doc --disable-debug && \ + ${{ github.workspace }}/ffmpeg-plugin/configure-ffmpeg.sh "7.0" --disable-doc --disable-debug && \ + rm -rf ${{ github.workspace }}/_build/mcm + echo "\"${{ github.workspace }}/ffmpeg-plugin/build-ffmpeg.sh\" \"6.1\"" > ${{ github.workspace }}/build.sh + echo "\"${{ github.workspace }}/ffmpeg-plugin/build-ffmpeg.sh\" \"7.0\"" > ${{ github.workspace }}/build.sh + + - name: "Run coverity" + uses: vapier/coverity-scan-action@2068473c7bdf8c2fb984a6a40ae76ee7facd7a85 # v1.8.0 + with: + project: "Media-Communications-Mesh" + email: ${{ secrets.COVERITY_SCAN_EMAIL }} + token: ${{ secrets.COVERITY_SCAN_TOKEN }} + build_language: "cxx" + build_platform: "linux64" + command: ${{ github.workspace }}/build.sh + + - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: coverity-reports + path: "${{ github.workspace }}/cov-int" diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 03e689ef0..e728ac86e 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -2,9 +2,9 @@ name: Docker Build on: pull_request: - branches: [ "main", "dev" ] + branches: ["main", "dev"] push: - branches: [ "main", "dev" ] + branches: ["main", "dev"] workflow_dispatch: permissions: @@ -19,14 +19,14 @@ jobs: name: Build sdk Docker Image uses: ./.github/workflows/build_docker_tpl.yml with: - docker_file_path: "sdk/Dockerfile" + docker_file_path: "sdk/Dockerfile" docker_image_name: "sdk" ffmpeg-6-1-image-build: name: Build ffmpeg v6.1 Docker Image uses: ./.github/workflows/build_docker_tpl.yml with: - docker_file_path: "ffmpeg-plugin/Dockerfile" + docker_file_path: "ffmpeg-plugin/Dockerfile" docker_image_name: "ffmpeg-6-1" docker_build_args: "FFMPEG_VER=6.1" @@ -34,7 +34,7 @@ jobs: name: Build ffmpeg v7.0 Docker Image uses: ./.github/workflows/build_docker_tpl.yml with: - docker_file_path: "ffmpeg-plugin/Dockerfile" + docker_file_path: "ffmpeg-plugin/Dockerfile" docker_image_name: "ffmpeg-7-0" docker_build_args: "FFMPEG_VER=7.0" @@ -42,5 +42,5 @@ jobs: name: Build Media-Proxy Docker Image uses: ./.github/workflows/build_docker_tpl.yml with: - docker_file_path: "media-proxy/Dockerfile" + docker_file_path: "media-proxy/Dockerfile" docker_image_name: "media-proxy" diff --git a/.github/workflows/github_pages_update.yml b/.github/workflows/github_pages_update.yml index 168255574..170e9f87d 100644 --- a/.github/workflows/github_pages_update.yml +++ b/.github/workflows/github_pages_update.yml @@ -3,7 +3,7 @@ on: workflow_call: workflow_dispatch: push: - branches: [ "main" ] + branches: ["main"] env: DEBIAN_FRONTEND: noninteractive @@ -45,7 +45,7 @@ jobs: run: python3 -m pip install sphinx_book_theme myst_parser sphinxcontrib.mermaid sphinx-copybutton - name: Build documentation - run: make -C docs/sphinx html + run: make -C docs/sphinx html - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@v3.0.1 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 9ae32d3e2..b225f6533 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -38,6 +38,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BASH_SEVERITY: "warning" LINTER_RULES_PATH: ".github/configs" + GITHUB_ACTIONS_CONFIG_FILE: "actionlint.yml" GITHUB_COMMENT_ON_PR: false GITHUB_STATUS_UPDATES: false VALIDATE_BASH_EXEC: true diff --git a/.github/workflows/report_summary.yml b/.github/workflows/report_summary.yml new file mode 100644 index 000000000..1e9302c03 --- /dev/null +++ b/.github/workflows/report_summary.yml @@ -0,0 +1,80 @@ +name: report-summary + +on: + workflow_call: + inputs: + artifact-name: + required: true + type: string + artifact-path: + required: false + type: string + +jobs: + summarize: + runs-on: ubuntu-latest + steps: + - name: Install jq + run: | + sudo apt-get update -y + sudo apt-get install -y jq + + - name: Download report artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: ${{ inputs.artifact-name }} + path: ./report_artifacts + + - name: Locate report.json + id: locate + run: | + if [ -n "${{ inputs.artifact-path }}" ]; then + REPORT_PATH="${{ inputs.artifact-path }}" + else + REPORT_PATH=$(find report_artifacts -type f -name 'report.json' | head -n1 || true) + fi + echo "REPORT_PATH=$REPORT_PATH" >> "$GITHUB_ENV" + if [ -z "$REPORT_PATH" ]; then + echo "No report.json found in downloaded artifacts" + exit 1 + fi + + - name: Add report to summary + if: always() + run: | + { + echo "## Smoke Tests Report" + echo "" + + # Check if JSON report exists + REPORT_FILE="$REPORT_PATH" + if [ -f "$REPORT_FILE" ]; then + # Parse JSON report + PASSED=$(jq '.summary.passed // 0' "$REPORT_FILE") + FAILED=$(jq '.summary.failed // 0' "$REPORT_FILE") + SKIPPED=$(jq '.summary.skipped // 0' "$REPORT_FILE") + ERROR=$(jq '.summary.errors // 0' "$REPORT_FILE") + + # Add summary stats + echo "| Status | Count |" + echo "| ------ | ----- |" + echo "| ✅ Passed | ${PASSED:-0} |" + echo "| ❌ Failed | ${FAILED:-0} |" + echo "| ⚠️ Error | ${ERROR:-0} |" + echo "| ⏭️ Skipped | ${SKIPPED:-0} |" + echo "" + + # Add test result details if available + TOTAL=$((${PASSED:-0} + ${FAILED:-0} + ${ERROR:-0} + ${SKIPPED:-0})) + echo "**Total Tests:** $TOTAL" + echo "" + if [ "${FAILED:-0}" -gt 0 ] || [ "${ERROR:-0}" -gt 0 ]; then + echo "❌ **Some tests failed!** Please check the detailed report." + else + echo "✅ **All tests passed!**" + fi + echo "" + else + echo "❌ No report.json file was generated" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3e7f940f1..569bb5565 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -6,10 +6,10 @@ on: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - - cron: '0 18 * * *' + - cron: "0 18 * * *" workflow_dispatch: push: - branches: [ "main" ] + branches: ["main"] permissions: contents: read diff --git a/.github/workflows/smoke_tests.yml b/.github/workflows/smoke_tests.yml new file mode 100644 index 000000000..87ccf145e --- /dev/null +++ b/.github/workflows/smoke_tests.yml @@ -0,0 +1,186 @@ +name: smoke-tests-bare-metal + +on: + push: + branches: ["main", "smoke-tests"] + paths-ignore: + - "**.md" + - "docs/**" + pull_request: + branches: ["main"] + paths-ignore: + - "**.md" + - "docs/**" + workflow_dispatch: + inputs: + branch-to-checkout: + type: string + default: "main" + required: false + description: "Branch name to use" + tag-to-checkout: + type: string + required: false + description: "Tag name to use" + list_tests: + type: choice + required: false + description: "List all tests before running" + options: + - "true" + - "false" + markers: + type: string + default: "smoke" + required: false + description: "Markers to use for pytest" + +env: + BUILD_TYPE: "Release" + DPDK_VERSION: "23.11" + DPDK_REBUILD: "false" + MEDIA_PATH: "/mnt/media" + BUILD_DIR: "${{ github.workspace }}/_build" + INPUT_PATH: "/mnt/media/" + OUTPUT_PATH: "${{ github.workspace }}/received/" + MCM_BINARIES_DIR: "./mcm-binaries" + MEDIA_PROXY: "./mcm-binaries/media_proxy" + MESH_AGENT: "./mcm-binaries/mesh-agent" + MCM_FFMPEG_7_0: "${{ github.workspace }}/_build/ffmpeg-7-0/ffmpeg" + MCM_LD_LIBRARY_PATH: "${{ github.workspace }}/_build/ffmpeg-7-0/lib/" + MTL_FFMPEG_7_0: "./mtl-binaries/ffmpeg-7-0/ffmpeg" + INTEGRITY_PATH: "${{ github.workspace }}/tests/validation/common/integrity" + LOG_DIR: "${{ github.workspace }}/logs" + +permissions: + contents: read + +concurrency: + group: smoke-tests-runner-4-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + call-bare-metal-build: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + uses: ./.github/workflows/bare-metal-build.yml + with: + branch: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.branch-to-checkout || 'main' }} + tag: ${{ github.event.inputs.tag-to-checkout }} + runner: runner-4 + + validation-prepare-setup-mcm: + runs-on: [self-hosted, runner-4] + needs: call-bare-metal-build + timeout-minutes: 60 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + outputs: + pipenv-activate: ${{ steps.pipenv-install.outputs.VIRTUAL_ENV }} + steps: + - name: "preparation: Harden Runner" + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + with: + egress-policy: audit + - name: "preparation: Restore valid repository owner and print env" + if: always() + run: | + sudo chown -R "${USER}" "$(pwd)" || true + env | grep BUILD_ || true + env | grep DPDK_ || true + - name: "Verify build artifacts" + run: | + # Just verify that binaries exist and are executable + echo "Verifying binaries in build directory..." + ls -la ${{ env.BUILD_DIR }}/ || true + ls -la ${{ env.BUILD_DIR }}/mtl/ || true + ls -la ${{ env.BUILD_DIR }}/mcm/bin/ || true + ls -la ${{ env.BUILD_DIR }}/mcm/bin/media_proxy || true + ls -la ${{ env.BUILD_DIR }}/mcm/bin/mesh-agent || true + ls -la ${{ env.BUILD_DIR }}/mcm/lib/libmcm_dp.so.* || true + ls -la ${{ env.BUILD_DIR }}/ffmpeg-6-1/ || true + ls -la ${{ env.BUILD_DIR }}/ffmpeg-7-0/ || true + + - name: "installation: Install pipenv environment" + working-directory: tests/validation + id: pipenv-install + run: | + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt + echo "VIRTUAL_ENV=$PWD/venv/bin/activate" >> "$GITHUB_ENV" + + - name: "add user name to environment and config" + run: | + echo "USER=${USER}" >> "$GITHUB_ENV" + sed -i "s/{{ USER }}/root/g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ KEY_PATH }}|/home/${USER}/.ssh/id_rsa|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ INTEGRITY_PATH }}|${{ env.INTEGRITY_PATH }}|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ OUTPUT_PATH }}|${{ env.OUTPUT_PATH }}|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ INPUT_PATH }}|${{ env.INPUT_PATH }}|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ MTL_PATH }}|${{ env.BUILD_DIR }}/mtl/|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ MCM_PATH }}|${{ github.workspace }}|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ MCM_FFMPEG_7_0 }}|${{ env.MCM_FFMPEG_7_0 }}|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ LD_LIBRARY_PATH }}|${{ env.MCM_LD_LIBRARY_PATH }}|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ OUTPUT_PATH }}|${{ github.workspace }}/received/|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ NICCTL_PATH }}|${{ env.BUILD_DIR }}/mtl/script/|g" tests/validation/configs/topology_config_workflow.yaml + sed -i "s|{{ LOG_PATH }}|${{ env.LOG_DIR }}|g" tests/validation/configs/test_config_workflow.yaml + + validation-run-tests: + needs: validation-prepare-setup-mcm + runs-on: [self-hosted, runner-4] + timeout-minutes: 60 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + env: + PYTEST_RETRIES: "3" + MARKERS: ${{ github.event.inputs.markers || 'smoke' }} + LIST_TESTS: ${{ github.event.inputs.list_tests || 'true' }} + steps: + - name: "preparation: Kill pytest routines" + run: | + sudo killall -SIGINT pipenv || true + sudo killall -SIGINT pytest || true + - name: "list all tests marked with ${{ env.MARKERS }}" + if: ${{ env.LIST_TESTS == 'true' }} + run: | + tests/validation/venv/bin/python3 -m pytest \ + --collect-only --quiet ./tests/validation/functional/ \ + -m "${{ env.MARKERS }}" + - name: "execution: Run validation-bare-metal tests in virtual environment" + run: | + tests/validation/venv/bin/python3 -m pytest \ + --topology_config=tests/validation/configs/topology_config_workflow.yaml \ + --test_config=tests/validation/configs/test_config_workflow.yaml \ + ./tests/validation/functional/ \ + --template=html/index.html --report=report.html \ + --json-report --json-report-file=report.json \ + --show-capture=no \ + -m "${{ env.MARKERS }}" + - name: "upload logs" + if: always() + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: smoke-tests-logs + path: | + ${{ env.LOG_DIR }}/* + + - name: "upload report" + if: always() + id: upload-report + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: smoke-tests-report + path: | + report.html + - name: "upload json report" + if: always() + id: upload-report-json + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: smoke-tests-report-json + path: | + report.json + + call-report-summary: + needs: validation-run-tests + uses: ./.github/workflows/report_summary.yml + with: + artifact-name: smoke-tests-report-json diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index c5192e927..d29e76a58 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -1,14 +1,14 @@ name: Trivy on: push: - branches: [ "main", "maint-*" ] + branches: ["main", "maint-*"] pull_request: - branches: [ "main", "maint-*" ] + branches: ["main", "maint-*"] workflow_dispatch: inputs: branch: - description: 'branch to run scans on' - default: 'main' + description: "branch to run scans on" + default: "main" type: string permissions: @@ -21,7 +21,7 @@ concurrency: jobs: scan: permissions: - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results runs-on: ubuntu-22.04 name: "Trivy: Perform scans job" steps: @@ -40,8 +40,8 @@ jobs: with: scan-type: config skip-dirs: deployment #helm charts not supported - exit-code: '0' - format: 'sarif' + exit-code: "0" + format: "sarif" output: "trivy-config-scan-results-${{ github.event.pull_request.number || github.sha }}.sarif" - name: "Trivy: Run vulnerability scanner for type=config (out=table)" @@ -50,8 +50,8 @@ jobs: with: scan-type: config skip-dirs: deployment #helm charts not supported - exit-code: '0' - format: 'table' + exit-code: "0" + format: "table" output: "trivy-config-scan-results-${{ github.event.pull_request.number || github.sha }}.txt" - name: "Trivy: Upload scan results to GitHub Security tab" diff --git a/.github/workflows/validation-tests.yml b/.github/workflows/validation-tests.yml index adcd1154e..cfc383093 100644 --- a/.github/workflows/validation-tests.yml +++ b/.github/workflows/validation-tests.yml @@ -6,13 +6,13 @@ on: inputs: branch-to-checkout: type: string - default: 'main' + default: "main" required: false - description: 'Branch name to use' + description: "Branch name to use" validation-iface-binding: type: choice required: true - description: 'Type of iface binding to use' + description: "Type of iface binding to use" options: - "create_vf" - "create_kvf" @@ -22,7 +22,7 @@ on: validation-test-port-p: type: choice required: true - description: 'Which to use as Test-Port-P' + description: "Which to use as Test-Port-P" options: - TEST_VF_PORT_P_0 - TEST_VF_PORT_P_1 @@ -37,7 +37,7 @@ on: validation-test-port-r: type: choice required: true - description: 'Which to use as Test-Port-R' + description: "Which to use as Test-Port-R" options: - TEST_VF_PORT_P_1 - TEST_VF_PORT_P_0 @@ -52,22 +52,22 @@ on: validation-no-fail-tests: type: choice required: false - description: 'Run all tests, non will fail' + description: "Run all tests, non will fail" options: - "true" - "false" validation-tests-1: type: string - default: 'single/video/pacing' + default: "single/video/pacing" required: true - description: '1st validation tests to run' + description: "1st validation tests to run" validation-tests-2: type: string - default: 'single/ancillary' + default: "single/ancillary" required: false - description: '2nd validation tests to run' + description: "2nd validation tests to run" validation-pre-release-1: - description: 'Select from pre-release group tests nr-1' + description: "Select from pre-release group tests nr-1" required: false type: choice options: @@ -82,7 +82,7 @@ on: - video - xdp validation-pre-release-2: - description: 'Select from pre-release group tests nr-2' + description: "Select from pre-release group tests nr-2" required: false type: choice options: @@ -96,7 +96,7 @@ on: - virtio-enable - wrong-parameter validation-pre-release-3: - description: 'Select from pre-release group tests nr-3' + description: "Select from pre-release group tests nr-3" required: false type: choice options: @@ -105,85 +105,85 @@ on: - gpu-enabling env: - BUILD_TYPE: 'Release' - DPDK_VERSION: '23.11' - DPDK_REBUILD: 'false' + BUILD_TYPE: "Release" + DPDK_VERSION: "23.11" + DPDK_REBUILD: "false" permissions: contents: read jobs: validation-build-mtm: - runs-on: [Linux, self-hosted, DPDK] + runs-on: [Linux, self-hosted] timeout-minutes: 60 outputs: pipenv-activate: ${{ steps.pipenv-install.outputs.VIRTUAL_ENV }} steps: - - name: 'preparation: Harden Runner' + - name: "preparation: Harden Runner" uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - name: 'Checkout repository' + - name: "Checkout repository" uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: 'Install OS level dependencies' + - name: "Install OS level dependencies" run: eval 'source scripts/setup_build_env.sh && install_package_dependencies' - - name: 'Check local dependencies build cache' + - name: "Check local dependencies build cache" id: load-local-dependencies-cache uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ env.BUILD_DIR }} key: ${{ runner.os }}-${{ hashFiles('versions.env') }}-${{ hashFiles('scripts/setup*.sh') }} - - name: 'Download, unpack and patch build dependencies' + - name: "Download, unpack and patch build dependencies" if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' run: eval 'source scripts/setup_build_env.sh && get_download_unpack_dependencies' - - name: 'Clone and patch ffmpeg 6.1 and 7.0' + - name: "Clone and patch ffmpeg 6.1 and 7.0" if: steps.load-local-dependencies-cache.outputs.cache-hit != 'true' run: | ffmpeg-plugin/clone-and-patch-ffmpeg.sh "6.1" ffmpeg-plugin/clone-and-patch-ffmpeg.sh "7.0" - - name: 'Build and Install xdp and libbpf' + - name: "Build and Install xdp and libbpf" run: eval 'source scripts/setup_build_env.sh && lib_install_xdp_bpf_tools' - - name: 'Build and Install libfabric' + - name: "Build and Install libfabric" run: eval 'source scripts/setup_build_env.sh && lib_install_fabrics' - - name: 'Build and Install the DPDK' + - name: "Build and Install the DPDK" run: eval 'source scripts/setup_build_env.sh && lib_install_dpdk' - - name: 'Build and Install the MTL' + - name: "Build and Install the MTL" run: eval 'source scripts/setup_build_env.sh && lib_install_mtl' - - name: 'Build and Install JPEG XS' + - name: "Build and Install JPEG XS" run: eval 'source scripts/setup_build_env.sh && lib_install_jpeg_xs' - - name: 'Build and Install JPEG XS ffmpeg plugin' + - name: "Build and Install JPEG XS ffmpeg plugin" run: eval 'source scripts/setup_build_env.sh && lib_install_mtl_jpeg_xs_plugin' - - name: 'Build gRPC and dependencies' + - name: "Build gRPC and dependencies" run: eval 'source scripts/setup_build_env.sh && lib_install_grpc' - - name: 'Build MCM SDK and Media Proxy' + - name: "Build MCM SDK and Media Proxy" run: eval 'source scripts/common.sh && ./build.sh "${PREFIX_DIR}"' - - name: 'Build FFmpeg 6.1 with MCM plugin' + - name: "Build FFmpeg 6.1 with MCM plugin" working-directory: ${{ github.workspace }}/ffmpeg-plugin run: | ./configure-ffmpeg.sh "6.1" --disable-doc --disable-debug && \ ./build-ffmpeg.sh "6.1" - - name: 'Build FFmpeg 7.0 with MCM plugin' + - name: "Build FFmpeg 7.0 with MCM plugin" working-directory: ${{ github.workspace }}/ffmpeg-plugin run: | ./configure-ffmpeg.sh "7.0" --disable-doc --disable-debug && \ ./build-ffmpeg.sh "7.0" - - name: 'installation: Install pipenv environment' + - name: "installation: Install pipenv environment" working-directory: tests/validation id: pipenv-install run: | @@ -194,19 +194,19 @@ jobs: # Timeout of this job is set to 12h [60m/h*12h=720m] validation-run-tests: needs: [validation-build-mtm] - runs-on: [Linux, self-hosted, DPDK] + runs-on: [Linux, self-hosted] timeout-minutes: 720 env: - PYTEST_ALIAS: 'sudo --preserve-env python3 -m pipenv run pytest' + PYTEST_ALIAS: "sudo --preserve-env python3 -m pipenv run pytest" PYTEST_PARAMS: '--media=/mnt/media --build="../.."' - PYTEST_RETRIES: '3' + PYTEST_RETRIES: "3" steps: - - name: 'preparation: Harden Runner' + - name: "preparation: Harden Runner" uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - name: 'cleanup: Generate runner summary' + - name: "cleanup: Generate runner summary" if: always() run: | { diff --git a/.gitignore b/.gitignore index cc9830ad5..b28288b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ cov-int* # Autogenerated version files mcm-version.h mcm-version.go + +.venv* \ No newline at end of file diff --git a/tests/validation/.gitignore b/tests/validation/.gitignore index 192e7bc5e..ee45cec74 100644 --- a/tests/validation/.gitignore +++ b/tests/validation/.gitignore @@ -1,5 +1,5 @@ .venv .vscode -logs +logs* pytest.log __pycache__ diff --git a/tests/validation/Engine/const.py b/tests/validation/Engine/const.py index 836855cf4..7d3531c1b 100644 --- a/tests/validation/Engine/const.py +++ b/tests/validation/Engine/const.py @@ -7,12 +7,17 @@ DEFAULT_INPUT_PATH = "/opt/intel/input_path/" DEFAULT_OUTPUT_PATH = "/opt/intel/output_path/" +FFMPEG_ESTABLISH_TIMEOUT = 6 # or use the same value as MCM_ESTABLISH_TIMEOUT +FFMPEG_RUN_TIMEOUT = 120 # or use the same value as MCM_RXTXAPP_RUN_TIMEOUT + +TESTCMD_LVL = 24 # Custom logging level for test commands + # time for establishing connection for example between TX and RX in st2110 MTL_ESTABLISH_TIMEOUT = 2 -MCM_ESTABLISH_TIMEOUT = 5 +MCM_ESTABLISH_TIMEOUT = 6 DEFAULT_LOOP_COUNT = 7 MCM_RXTXAPP_RUN_TIMEOUT = MCM_ESTABLISH_TIMEOUT * DEFAULT_LOOP_COUNT -MAX_TEST_TIME_DEFAULT = 60 +MAX_TEST_TIME_DEFAULT = 120 STOP_GRACEFULLY_PERIOD = 2 # seconds BUILD_DIR = "_build" @@ -48,9 +53,9 @@ MESH_AGENT_ERROR_KEYWORDS = ["[ERRO]"] RX_TX_APP_ERROR_KEYWORDS = ["[ERRO]"] -DEFAULT_MPG_URN = "ipv4:224.0.0.1:9003" -DEFAULT_REMOTE_IP_ADDR = "239.2.39.238" DEFAULT_REMOTE_PORT = 20000 +DEFAULT_MPG_URN = f"ipv4:224.0.0.1:{DEFAULT_REMOTE_PORT}" +DEFAULT_REMOTE_IP_ADDR = "239.2.39.238" DEFAULT_PACING = "narrow" DEFAULT_PAYLOAD_TYPE_ST2110_20 = 112 DEFAULT_PAYLOAD_TYPE_ST2110_30 = 111 diff --git a/tests/validation/Engine/mcm_apps.py b/tests/validation/Engine/mcm_apps.py index 1f12c85da..a7ec19d4c 100644 --- a/tests/validation/Engine/mcm_apps.py +++ b/tests/validation/Engine/mcm_apps.py @@ -17,6 +17,21 @@ logger = logging.getLogger(__name__) +def get_log_folder_path(test_config: dict) -> Path: + """ + Returns the path to the log folder on the given host. + If the host has a custom log folder path set in its extra_info, it will return that. + Otherwise, it returns the default log folder path. + """ + validation_dir = Path(__file__).parent.parent + log_path = test_config.get("log_path") + default_log_path = Path(validation_dir, LOG_FOLDER) + + if log_path: + return Path(log_path) + return default_log_path + + def get_mtl_path(host) -> str: """ Returns the path to the Media Transport Library (MTL) on the given host. @@ -71,7 +86,7 @@ def save_process_log( write_cmd = True with open(log_file, "a") as f: if write_cmd: - f.write(cmd + "\n\n") + f.write((cmd if cmd is not None else "") + "\n\n") f.write(cleaned_text + "\n") @@ -108,6 +123,9 @@ def output_validator( with open(log_file_path, "r") as f: output = f.read() + if output is None: + output = "" + errors = [] phrase_mismatches = [] diff --git a/tests/validation/Engine/rx_tx_app_client_json.py b/tests/validation/Engine/rx_tx_app_client_json.py index 46421435d..f0bbe9722 100644 --- a/tests/validation/Engine/rx_tx_app_client_json.py +++ b/tests/validation/Engine/rx_tx_app_client_json.py @@ -3,6 +3,7 @@ # Media Communications Mesh import json +from pathlib import Path class ClientJson: @@ -47,3 +48,15 @@ def prepare_and_save_json(self, output_path: str = "client.json") -> None: f = self.host.connection.path(output_path) json_content = self.to_json().replace('"', '\\"') f.write_text(json_content) + + def copy_json_to_logs(self, log_path: str) -> None: + """Copy the client.json file to the log path on runner.""" + source_path = self.host.connection.path("client.json") + dest_path = Path(log_path) / "client.json" + + # Create log directory if it doesn't exist + Path(log_path).mkdir(parents=True, exist_ok=True) + + # Copy the client.json file to the log path + with open(dest_path, "w") as dest_file: + dest_file.write(self.to_json()) diff --git a/tests/validation/Engine/rx_tx_app_connection_json.py b/tests/validation/Engine/rx_tx_app_connection_json.py index 797564709..9e38665f8 100644 --- a/tests/validation/Engine/rx_tx_app_connection_json.py +++ b/tests/validation/Engine/rx_tx_app_connection_json.py @@ -3,6 +3,7 @@ # Media Communications Mesh import json +from pathlib import Path from .rx_tx_app_connection import RxTxAppConnection from .rx_tx_app_payload import Payload @@ -45,3 +46,15 @@ def prepare_and_save_json(self, output_path: str = "connection.json") -> None: f = self.host.connection.path(output_path) json_content = self.to_json().replace('"', '\\"') f.write_text(json_content) + + def copy_json_to_logs(self, log_path: str) -> None: + """Copy the connection.json file to the log path on runner.""" + source_path = self.host.connection.path("connection.json") + dest_path = Path(log_path) / "connection.json" + + # Create log directory if it doesn't exist + Path(log_path).mkdir(parents=True, exist_ok=True) + + # Copy the connection.json file to the log path + with open(dest_path, "w") as dest_file: + dest_file.write(self.to_json()) diff --git a/tests/validation/Engine/rx_tx_app_engine_mcm.py b/tests/validation/Engine/rx_tx_app_engine_mcm.py index c15294702..ce1f7f3f2 100644 --- a/tests/validation/Engine/rx_tx_app_engine_mcm.py +++ b/tests/validation/Engine/rx_tx_app_engine_mcm.py @@ -11,20 +11,24 @@ import Engine.rx_tx_app_client_json import Engine.rx_tx_app_connection_json -from Engine.const import LOG_FOLDER, RX_TX_APP_ERROR_KEYWORDS, DEFAULT_OUTPUT_PATH +from Engine.const import LOG_FOLDER, DEFAULT_OUTPUT_PATH, TESTCMD_LVL +from common.log_constants import ( + RX_REQUIRED_LOG_PHRASES, + TX_REQUIRED_LOG_PHRASES, + RX_TX_APP_ERROR_KEYWORDS, +) from Engine.mcm_apps import ( get_media_proxy_port, - output_validator, save_process_log, get_mcm_path, ) -from Engine.rx_tx_app_file_validation_utils import validate_file +from Engine.rx_tx_app_file_validation_utils import validate_file, cleanup_file logger = logging.getLogger(__name__) def create_client_json( - build: str, client: Engine.rx_tx_app_client_json.ClientJson + build: str, client: Engine.rx_tx_app_client_json.ClientJson, log_path: str = "" ) -> None: logger.debug("Client JSON:") for line in client.to_json().splitlines(): @@ -32,10 +36,15 @@ def create_client_json( output_path = str(Path(build, "tests", "tools", "TestApp", "build", "client.json")) logger.debug(f"Client JSON path: {output_path}") client.prepare_and_save_json(output_path=output_path) + # Use provided log_path or default to LOG_FOLDER + log_dir = log_path if log_path else LOG_FOLDER + client.copy_json_to_logs(log_path=log_dir) def create_connection_json( - build: str, rx_tx_app_connection: Engine.rx_tx_app_connection_json.ConnectionJson + build: str, + rx_tx_app_connection: Engine.rx_tx_app_connection_json.ConnectionJson, + log_path: str = "", ) -> None: logger.debug("Connection JSON:") for line in rx_tx_app_connection.to_json().splitlines(): @@ -45,6 +54,9 @@ def create_connection_json( ) logger.debug(f"Connection JSON path: {output_path}") rx_tx_app_connection.prepare_and_save_json(output_path=output_path) + # Use provided log_path or default to LOG_FOLDER + log_dir = log_path if log_path else LOG_FOLDER + rx_tx_app_connection.copy_json_to_logs(log_path=log_dir) class AppRunnerBase: @@ -164,13 +176,19 @@ def _ensure_output_directory_exists(self): logger.warning(f"Error creating directory {output_dir}: {str(e)}") def start(self): - create_client_json(self.mcm_path, self.rx_tx_app_client_json) - create_connection_json(self.mcm_path, self.rx_tx_app_connection_json) + # Use self.log_path for consistent logging across the application + log_dir = self.log_path if self.log_path else LOG_FOLDER + create_client_json(self.mcm_path, self.rx_tx_app_client_json, log_path=log_dir) + create_connection_json( + self.mcm_path, self.rx_tx_app_connection_json, log_path=log_dir + ) self._ensure_output_directory_exists() def stop(self): validation_info = [] file_validation_passed = True + app_log_validation_status = False + app_log_error_count = 0 if self.process: try: @@ -183,40 +201,78 @@ def stop(self): logger.info("Process has already finished (nothing to stop).") logger.info(f"{self.direction} app stopped.") - log_dir = self.log_path if self.log_path else LOG_FOLDER + log_dir = self.log_path if self.log_path is not None else LOG_FOLDER subdir = f"RxTx/{self.host.name}" filename = f"{self.direction.lower()}.log" log_file_path = os.path.join(log_dir, subdir, filename) - result = output_validator( - log_file_path=log_file_path, - error_keywords=RX_TX_APP_ERROR_KEYWORDS, - ) - if result["errors"]: - logger.warning(f"Errors found: {result['errors']}") + app_log_validation_status = False + app_log_error_count = 0 + # Using common log validation utility + from common.log_validation_utils import check_phrases_in_order - self.is_pass = result["is_pass"] + if self.direction in ("Rx", "Tx"): + from common.log_validation_utils import validate_log_file - # Collect log validation info - validation_info.append(f"=== {self.direction} App Log Validation ===") - validation_info.append(f"Log file: {log_file_path}") - validation_info.append( - f"Validation result: {'PASS' if result['is_pass'] else 'FAIL'}" - ) - validation_info.append(f"Errors found: {len(result['errors'])}") - if result["errors"]: - validation_info.append("Error details:") - for error in result["errors"]: - validation_info.append(f" - {error}") - if result["phrase_mismatches"]: - validation_info.append("Phrase mismatches:") - for phrase, found, expected in result["phrase_mismatches"]: - validation_info.append( - f" - {phrase}: found '{found}', expected '{expected}'" + required_phrases = ( + RX_REQUIRED_LOG_PHRASES + if self.direction == "Rx" + else TX_REQUIRED_LOG_PHRASES + ) + + validation_result = validate_log_file( + log_file_path, required_phrases, self.direction, strict_order=False + ) + + self.is_pass = validation_result["is_pass"] + app_log_validation_status = validation_result["is_pass"] + app_log_error_count = validation_result["error_count"] + validation_info.extend(validation_result["validation_info"]) + + # Additional logging if validation failed + if ( + not validation_result["is_pass"] + and validation_result["missing_phrases"] + ): + print( + f"{self.direction} process did not pass. First missing phrase: {validation_result['missing_phrases'][0]}" ) + else: + from common.log_validation_utils import output_validator - # File validation for Rx only run if output path isn't "/dev/null" - if self.direction == "Rx" and self.output and self.output_path != "/dev/null": + result = output_validator( + log_file_path=log_file_path, + error_keywords=RX_TX_APP_ERROR_KEYWORDS, + ) + if result["errors"]: + logger.warning(f"Errors found: {result['errors']}") + self.is_pass = result["is_pass"] + app_log_validation_status = result["is_pass"] + app_log_error_count = len(result["errors"]) + validation_info.append(f"=== {self.direction} App Log Validation ===") + validation_info.append(f"Log file: {log_file_path}") + validation_info.append( + f"Validation result: {'PASS' if result['is_pass'] else 'FAIL'}" + ) + validation_info.append(f"Errors found: {len(result['errors'])}") + if result["errors"]: + validation_info.append("Error details:") + for error in result["errors"]: + validation_info.append(f" - {error}") + if result["phrase_mismatches"]: + validation_info.append("Phrase mismatches:") + for phrase, found, expected in result["phrase_mismatches"]: + validation_info.append( + f" - {phrase}: found '{found}', expected '{expected}'" + ) + + # File validation for Rx only run if output path isn't "/dev/null" or doesn't start with "/dev/null/" + if ( + self.direction == "Rx" + and self.output + and self.output_path + and not str(self.output_path).startswith("/dev/null") + ): validation_info.append(f"\n=== {self.direction} Output File Validation ===") validation_info.append(f"Expected output file: {self.output}") @@ -234,7 +290,7 @@ def stop(self): ) validation_info.append(f"Overall validation: {overall_status}") validation_info.append( - f"App log validation: {'PASS' if result['is_pass'] else 'FAIL'}" + f"App log validation: {'PASS' if app_log_validation_status else 'FAIL'}" ) if self.direction == "Rx": file_status = "PASS" if file_validation_passed else "FAIL" @@ -245,8 +301,7 @@ def stop(self): "Note: Overall validation fails if either app log or file validation fails" ) - # Save to validation report file - log_dir = self.log_path if self.log_path else LOG_FOLDER + log_dir = self.log_path if self.log_path is not None else LOG_FOLDER subdir = f"RxTx/{self.host.name}" validation_filename = f"{self.direction.lower()}_validation.log" @@ -279,22 +334,20 @@ def start(self): f"Starting Tx app with payload: {self.payload.payload_type} on {self.host}" ) cmd = self._get_app_cmd("Tx") + logger.log(TESTCMD_LVL, f"Tx command: {cmd}") self.process = self.host.connection.start_process( cmd, shell=True, stderr_to_stdout=True, cwd=self.app_path ) - # Start background logging thread - subdir = f"RxTx/{self.host.name}" - filename = "tx.log" - def log_output(): + log_dir = self.log_path if self.log_path is not None else LOG_FOLDER for line in self.process.get_stdout_iter(): save_process_log( - subdir=subdir, - filename=filename, + subdir=f"RxTx/{self.host.name}", + filename="tx.log", text=line.rstrip(), cmd=cmd, - log_dir=self.log_path, + log_dir=log_dir, ) threading.Thread(target=log_output, daemon=True).start() @@ -334,22 +387,20 @@ def start(self): f"Starting Rx app with payload: {self.payload.payload_type} on {self.host}" ) cmd = self._get_app_cmd("Rx") + logger.log(TESTCMD_LVL, f"Rx command: {cmd}") self.process = self.host.connection.start_process( cmd, shell=True, stderr_to_stdout=True, cwd=self.app_path ) - # Start background logging thread - subdir = f"RxTx/{self.host.name}" - filename = "rx.log" - def log_output(): + log_dir = self.log_path if self.log_path is not None else LOG_FOLDER for line in self.process.get_stdout_iter(): save_process_log( - subdir=subdir, - filename=filename, + subdir=f"RxTx/{self.host.name}", + filename="rx.log", text=line.rstrip(), cmd=cmd, - log_dir=self.log_path, + log_dir=log_dir, ) threading.Thread(target=log_output, daemon=True).start() @@ -357,8 +408,6 @@ def log_output(): def cleanup(self): """Clean up the output file created by the Rx app.""" - from Engine.rx_tx_app_file_validation_utils import cleanup_file - if self.output: success = cleanup_file(self.host.connection, str(self.output)) if success: diff --git a/tests/validation/README.md b/tests/validation/README.md index 09f7df1ab..6e59a6a33 100644 --- a/tests/validation/README.md +++ b/tests/validation/README.md @@ -65,9 +65,12 @@ To manually run the `test_blob_25_03` test from `test_blob_25_03.py` with the pa ```bash sudo .venv/bin/python3 -m pytest \ - --topology_config=./configs/topology_config_workflow.yaml \ - --test_config=./configs/test_config_workflow.yaml \ - ./functional/local/blob/test_blob_25_03.py::test_blob_25_03[|file = random_bin_100M|] + --topology_config=./configs/topology_config.yaml \ + --test_config=./configs/test_config.yaml \ + ./functional/local/blob/test_blob_25_03.py::test_blob_25_03[|file = random_bin_100M|] \ + --template=html/index.html --report=report.html \ + --json-report --json-report-file=report.json \ + --show-capture=no ``` To collect all smoke tests use: diff --git a/tests/validation/common/__init__.py b/tests/validation/common/__init__.py index e69de29bb..a91ab25b6 100644 --- a/tests/validation/common/__init__.py +++ b/tests/validation/common/__init__.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2024-2025 Intel Corporation +# Media Communications Mesh + +""" +Common utilities package for validation code. +""" + +from common.log_validation_utils import ( + check_phrases_in_order, + validate_log_file, + output_validator, +) +from common.log_constants import ( + RX_REQUIRED_LOG_PHRASES, + TX_REQUIRED_LOG_PHRASES, + RX_TX_APP_ERROR_KEYWORDS, +) + +__all__ = [ + "check_phrases_in_order", + "validate_log_file", + "output_validator", + "RX_REQUIRED_LOG_PHRASES", + "TX_REQUIRED_LOG_PHRASES", + "RX_TX_APP_ERROR_KEYWORDS", +] diff --git a/tests/validation/common/ffmpeg_handler/__init__.py b/tests/validation/common/ffmpeg_handler/__init__.py index e69de29bb..4e77f3267 100644 --- a/tests/validation/common/ffmpeg_handler/__init__.py +++ b/tests/validation/common/ffmpeg_handler/__init__.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2024-2025 Intel Corporation +# Media Communications Mesh + +""" +FFmpeg handler package for Media Communications Mesh validation. +""" + +from common.ffmpeg_handler.log_constants import ( + FFMPEG_RX_REQUIRED_LOG_PHRASES, + FFMPEG_TX_REQUIRED_LOG_PHRASES, + FFMPEG_ERROR_KEYWORDS, +) + +__all__ = [ + "FFMPEG_RX_REQUIRED_LOG_PHRASES", + "FFMPEG_TX_REQUIRED_LOG_PHRASES", + "FFMPEG_ERROR_KEYWORDS", +] diff --git a/tests/validation/common/ffmpeg_handler/ffmpeg.py b/tests/validation/common/ffmpeg_handler/ffmpeg.py index 33bc8b38f..ac5896b97 100644 --- a/tests/validation/common/ffmpeg_handler/ffmpeg.py +++ b/tests/validation/common/ffmpeg_handler/ffmpeg.py @@ -2,6 +2,7 @@ # Copyright 2025 Intel Corporation # Media Communications Mesh import logging +import os import threading import time @@ -9,8 +10,9 @@ SSHRemoteProcessEndException, RemoteProcessInvalidState, ) -from Engine.const import LOG_FOLDER +from Engine.const import LOG_FOLDER, TESTCMD_LVL from Engine.mcm_apps import save_process_log +from Engine.rx_tx_app_file_validation_utils import cleanup_file from .ffmpeg_io import FFmpegIO @@ -73,10 +75,142 @@ def __init__(self, host, ffmpeg_instance: FFmpeg, log_path=None): self.ff = ffmpeg_instance self.log_path = log_path self._processes = [] + self.is_pass = False + + def validate(self): + """ + Validates the FFmpeg process execution and output. + + Performs two types of validation: + 1. Log validation - checks for required phrases and error keywords + 2. File validation - checks if the output file exists and has expected characteristics + + Generates validation report files. + + Returns: + bool: True if validation passed, False otherwise + """ + process_passed = True + validation_info = [] + + for process in self._processes: + if process.return_code != 0: + logger.warning( + f"FFmpeg process on {self.host.name} failed with return code {process.return_code}" + ) + process_passed = False + + # Determine if this is a receiver or transmitter + is_receiver = False + if self.ff.ffmpeg_input and self.ff.ffmpeg_output: + input_path = getattr(self.ff.ffmpeg_input, "input_path", None) + output_path = getattr(self.ff.ffmpeg_output, "output_path", None) + + if input_path == "-" or ( + output_path and output_path != "-" and "." in output_path + ): + is_receiver = True + + direction = "Rx" if is_receiver else "Tx" + + # Find the log file + log_dir = self.log_path if self.log_path else LOG_FOLDER + subdir = f"RxTx/{self.host.name}" + input_class_name = None + if self.ff.ffmpeg_input: + input_class_name = self.ff.ffmpeg_input.__class__.__name__ + prefix = "mtl_" if input_class_name and "Mtl" in input_class_name else "mcm_" + log_filename = prefix + ("ffmpeg_rx.log" if is_receiver else "ffmpeg_tx.log") + + log_file_path = os.path.join(log_dir, subdir, log_filename) + + # Perform log validation + from common.log_validation_utils import validate_log_file + from common.ffmpeg_handler.log_constants import ( + FFMPEG_RX_REQUIRED_LOG_PHRASES, + FFMPEG_TX_REQUIRED_LOG_PHRASES, + FFMPEG_ERROR_KEYWORDS, + ) + + required_phrases = ( + FFMPEG_RX_REQUIRED_LOG_PHRASES + if is_receiver + else FFMPEG_TX_REQUIRED_LOG_PHRASES + ) + + if os.path.exists(log_file_path): + validation_result = validate_log_file( + log_file_path, required_phrases, direction, FFMPEG_ERROR_KEYWORDS + ) + + log_validation_passed = validation_result["is_pass"] + validation_info.extend(validation_result["validation_info"]) + else: + logger.warning(f"Log file not found at {log_file_path}") + validation_info.append(f"=== {direction} Log Validation ===") + validation_info.append(f"Log file: {log_file_path}") + validation_info.append(f"Validation result: FAIL") + validation_info.append(f"Errors found: 1") + validation_info.append(f"Missing log file") + log_validation_passed = False + + # File validation for Rx only run if output path isn't "/dev/null" or doesn't start with "/dev/null/" + file_validation_passed = True + if ( + is_receiver + and self.ff.ffmpeg_output + and hasattr(self.ff.ffmpeg_output, "output_path") + ): + output_path = self.ff.ffmpeg_output.output_path + if output_path and not str(output_path).startswith("/dev/null"): + validation_info.append(f"\n=== {direction} Output File Validation ===") + validation_info.append(f"Expected output file: {output_path}") + + from Engine.rx_tx_app_file_validation_utils import validate_file + + file_info, file_validation_passed = validate_file( + self.host.connection, output_path, cleanup=False + ) + validation_info.extend(file_info) + + # Overall validation status + self.is_pass = ( + process_passed and log_validation_passed and file_validation_passed + ) + + # Save validation report + validation_info.append(f"\n=== Overall Validation Summary ===") + validation_info.append( + f"Overall validation: {'PASS' if self.is_pass else 'FAIL'}" + ) + validation_info.append( + f"Process validation: {'PASS' if process_passed else 'FAIL'}" + ) + validation_info.append( + f"Log validation: {'PASS' if log_validation_passed else 'FAIL'}" + ) + if is_receiver: + validation_info.append( + f"File validation: {'PASS' if file_validation_passed else 'FAIL'}" + ) + validation_info.append( + f"Note: Overall validation fails if any validation step fails" + ) + + # Save validation report to a file + from common.log_validation_utils import save_validation_report + + validation_path = os.path.join( + log_dir, subdir, f"{direction.lower()}_validation.log" + ) + save_validation_report(validation_path, validation_info, self.is_pass) + + return self.is_pass def start(self): """Starts the FFmpeg process on the host, waits for the process to start.""" cmd = self.ff.get_command() + logger.log(TESTCMD_LVL, f"ffmpeg command: {cmd}") ffmpeg_process = self.host.connection.start_process( cmd, stderr_to_stdout=True, shell=True ) @@ -118,6 +252,7 @@ def start(self): filename = prefix + filename def log_output(): + log_dir = self.log_path if self.log_path is not None else LOG_FOLDER for line in ffmpeg_process.get_stdout_iter(): save_process_log( subdir=subdir, @@ -171,6 +306,46 @@ def stop(self, wait: float = 0.0) -> float: logger.info("No FFmpeg process to stop!") return elapsed + def wait_with_timeout(self, timeout=None): + """Wait for the process to complete with a timeout.""" + if not timeout: + timeout = ( + 60 # Default timeout or use a constant like MCM_RXTXAPP_RUN_TIMEOUT + ) + + try: + for process in self._processes: + if process.running: + process.wait(timeout=timeout) + except Exception as e: + logger.warning( + f"FFmpeg process did not finish in time or error occurred: {e}" + ) + return False + return True + + def cleanup(self): + """Clean up any resources or output files.""" + if ( + self.ff.ffmpeg_output + and hasattr(self.ff.ffmpeg_output, "output_path") + and self.ff.ffmpeg_output.output_path + and self.ff.ffmpeg_output.output_path != "-" + and not str(self.ff.ffmpeg_output.output_path).startswith("/dev/null") + ): + + success = cleanup_file( + self.host.connection, str(self.ff.ffmpeg_output.output_path) + ) + if success: + logger.debug( + f"Cleaned up output file: {self.ff.ffmpeg_output.output_path}" + ) + else: + logger.warning( + f"Failed to clean up output file: {self.ff.ffmpeg_output.output_path}" + ) + def no_proxy_to_prefix_variables(host, prefix_variables: dict | None = None): """ diff --git a/tests/validation/common/ffmpeg_handler/log_constants.py b/tests/validation/common/ffmpeg_handler/log_constants.py new file mode 100644 index 000000000..5fd82d4b1 --- /dev/null +++ b/tests/validation/common/ffmpeg_handler/log_constants.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2024-2025 Intel Corporation +# Media Communications Mesh + +""" +Log validation constants for FFmpeg validation. +""" + +# Required ordered log phrases for FFmpeg Rx validation +FFMPEG_RX_REQUIRED_LOG_PHRASES = [ + "[DEBU] JSON client config:", + "[INFO] Media Communications Mesh SDK version", + "[DEBU] JSON conn config:", + "[INFO] gRPC: connection created", + "INFO - Create memif socket.", + "INFO - Create memif interface.", + "INFO - memif connected!", + "[INFO] gRPC: connection active", +] + +# Required ordered log phrases for FFmpeg Tx validation +FFMPEG_TX_REQUIRED_LOG_PHRASES = [ + "[DEBU] JSON client config:", + "[INFO] Media Communications Mesh SDK version", + "[DEBU] JSON conn config:", + "[DEBU] BUF PARTS", +] + +# Common error keywords to look for in logs +FFMPEG_ERROR_KEYWORDS = [ + "ERROR", + "FATAL", + "exception", + "segfault", + "core dumped", + "failed", + "FAIL", + "[error]", +] diff --git a/tests/validation/common/log_constants.py b/tests/validation/common/log_constants.py new file mode 100644 index 000000000..351f45176 --- /dev/null +++ b/tests/validation/common/log_constants.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2024-2025 Intel Corporation +# Media Communications Mesh + +""" +Common constants for log validation used by both rxtxapp and ffmpeg validation. +""" + +# Required ordered log phrases for Rx validation +RX_REQUIRED_LOG_PHRASES = [ + "[RX] Reading client configuration", + "[RX] Reading connection configuration", + "[DEBU] JSON client config:", + "[INFO] Media Communications Mesh SDK version", + "[DEBU] JSON conn config:", + "[RX] Fetched mesh data buffer", + "[RX] Saving buffer data to a file", + "[RX] Done reading the data", + "[RX] dropping connection to media-proxy", + "INFO - memif disconnected!", +] + +# Required ordered log phrases for Tx validation +TX_REQUIRED_LOG_PHRASES = [ + "[TX] Reading client configuration", + "[TX] Reading connection configuration", + "[DEBU] JSON client config:", + "[INFO] Media Communications Mesh SDK version", + "[DEBU] JSON conn config:", + "[INFO] gRPC: connection created", + "INFO - Create memif socket.", + "INFO - Create memif interface.", +] + +# Common error keywords to look for in logs +RX_TX_APP_ERROR_KEYWORDS = [ + "ERROR", + "FATAL", + "exception", + "segfault", + "core dumped", + "failed", + "FAIL", +] diff --git a/tests/validation/common/log_validation_utils.py b/tests/validation/common/log_validation_utils.py new file mode 100644 index 000000000..b51c7f155 --- /dev/null +++ b/tests/validation/common/log_validation_utils.py @@ -0,0 +1,290 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2024-2025 Intel Corporation +# Media Communications Mesh + +""" +Common log validation utilities for use by both rxtxapp and ffmpeg validation. +""" +import logging +import os +from typing import List, Tuple, Dict, Any, Optional + +from common.log_constants import RX_TX_APP_ERROR_KEYWORDS + +logger = logging.getLogger(__name__) + + +def check_phrases_in_order( + log_path: str, phrases: List[str] +) -> Tuple[bool, List[str], Dict[str, List[str]]]: + """ + Check that all required phrases appear in order in the log file. + Returns (all_found, missing_phrases, context_lines) + """ + found_indices = [] + missing_phrases = [] + lines_around_missing = {} + with open(log_path, "r", encoding="utf-8", errors="ignore") as f: + lines = [line.strip() for line in f] + + idx = 0 + for phrase_idx, phrase in enumerate(phrases): + found = False + phrase_stripped = phrase.strip() + start_idx = idx # Remember where we started searching + + while idx < len(lines): + line_stripped = lines[idx].strip() + if phrase_stripped in line_stripped: + found_indices.append(idx) + found = True + idx += 1 + break + idx += 1 + + if not found: + missing_phrases.append(phrase) + # Store context - lines around where we were searching + context_start = max(0, start_idx - 3) + context_end = min(len(lines), start_idx + 7) + lines_around_missing[phrase] = lines[context_start:context_end] + + return len(missing_phrases) == 0, missing_phrases, lines_around_missing + + +def check_for_errors( + log_path: str, error_keywords: Optional[List[str]] = None +) -> Tuple[bool, List[Dict[str, Any]]]: + """ + Check the log file for error keywords. + + Args: + log_path: Path to the log file + error_keywords: List of keywords indicating errors (default: RX_TX_APP_ERROR_KEYWORDS) + + Returns: + Tuple of (is_pass, errors_found) + errors_found is a list of dicts with 'line', 'line_number', and 'keyword' keys + """ + if error_keywords is None: + error_keywords = RX_TX_APP_ERROR_KEYWORDS + + errors = [] + if not os.path.exists(log_path): + logger.error(f"Log file not found: {log_path}") + return False, [ + { + "line": "Log file not found", + "line_number": 0, + "keyword": "FILE_NOT_FOUND", + } + ] + + try: + with open(log_path, "r", encoding="utf-8", errors="replace") as f: + for i, line in enumerate(f): + for keyword in error_keywords: + if keyword.lower() in line.lower(): + # Ignore certain false positives + if "ERROR" in keyword and ( + "NO ERROR" in line.upper() or "NO_ERROR" in line.upper() + ): + continue + errors.append( + { + "line": line.strip(), + "line_number": i + 1, + "keyword": keyword, + } + ) + break + except Exception as e: + logger.error(f"Error reading log file: {e}") + return False, [ + { + "line": f"Error reading log file: {e}", + "line_number": 0, + "keyword": "FILE_READ_ERROR", + } + ] + + return len(errors) == 0, errors + + +def validate_log_file( + log_file_path: str, + required_phrases: List[str], + direction: str = "", + error_keywords: Optional[List[str]] = None, + strict_order: bool = False, +) -> Dict: + """ + Validate log file for required phrases and return validation information. + + Args: + log_file_path: Path to the log file + required_phrases: List of phrases to check for + direction: Optional string to identify the direction (e.g., 'Rx', 'Tx') + error_keywords: Optional list of error keywords to check for + strict_order: Whether to check phrases in strict order (default: False) + + Returns: + Dictionary containing validation results: + { + 'is_pass': bool, + 'error_count': int, + 'validation_info': list of validation information strings, + 'missing_phrases': list of missing phrases, + 'context_lines': dict mapping phrases to context lines, + 'errors': list of error dictionaries (if error_keywords provided) + } + """ + validation_info = [] + + # Phrase validation (either ordered or anywhere) + if strict_order: + log_pass, missing, context_lines = check_phrases_in_order( + log_file_path, required_phrases + ) + else: + log_pass, missing, context_lines = check_phrases_anywhere( + log_file_path, required_phrases + ) + error_count = len(missing) + + # Error keyword validation (optional) + error_check_pass = True + errors = [] + if error_keywords is not None: + error_check_pass, errors = check_for_errors(log_file_path, error_keywords) + error_count += len(errors) + + # Overall pass/fail status + is_pass = log_pass and error_check_pass + + # Build validation info + dir_prefix = f"{direction} " if direction else "" + validation_info.append(f"=== {dir_prefix}Log Validation ===") + validation_info.append(f"Log file: {log_file_path}") + validation_info.append(f"Validation result: {'PASS' if is_pass else 'FAIL'}") + validation_info.append(f"Total errors found: {error_count}") + + # Missing phrases info + if not log_pass: + validation_info.append(f"Missing or out-of-order phrases analysis:") + for phrase in missing: + validation_info.append(f'\n Expected phrase: "{phrase}"') + validation_info.append(f" Context in log file:") + if phrase in context_lines: + for line in context_lines[phrase]: + validation_info.append(f" {line}") + else: + validation_info.append(" ") + if missing: + logger.warning( + f"{dir_prefix}process did not pass. First missing phrase: {missing[0]}" + ) + + # Error keywords info + if errors: + validation_info.append(f"\nError keywords found:") + for error in errors: + validation_info.append( + f" Line {error['line_number']} - {error['keyword']}: {error['line']}" + ) + + return { + "is_pass": is_pass, + "error_count": error_count, + "validation_info": validation_info, + "missing_phrases": missing, + "context_lines": context_lines, + "errors": errors, + } + + +def save_validation_report( + report_path: str, validation_info: List[str], overall_status: bool +) -> None: + """ + Save validation report to a file. + + Args: + report_path: Path where to save the report + validation_info: List of validation information strings + overall_status: Overall pass/fail status + """ + try: + os.makedirs(os.path.dirname(report_path), exist_ok=True) + with open(report_path, "w", encoding="utf-8") as f: + for line in validation_info: + f.write(f"{line}\n") + f.write(f"\n=== Overall Validation Summary ===\n") + f.write(f"Overall result: {'PASS' if overall_status else 'FAIL'}\n") + logger.info(f"Validation report saved to {report_path}") + except Exception as e: + logger.error(f"Error saving validation report: {e}") + + +def check_phrases_anywhere( + log_path: str, phrases: List[str] +) -> Tuple[bool, List[str], Dict[str, List[str]]]: + """ + Check that all required phrases appear anywhere in the log file, regardless of order. + Returns (all_found, missing_phrases, context_lines) + """ + missing_phrases = [] + lines_around_missing = {} + + try: + with open(log_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + lines = content.split("\n") + + for phrase in phrases: + # Check if the phrase is contained in any line, not just the entire content + phrase_found = False + for line in lines: + if phrase in line: + phrase_found = True + break + + if not phrase_found: + missing_phrases.append(phrase) + # Find where the phrase should have appeared + # Just give some context from the end of the log + context_end = len(lines) + context_start = max(0, context_end - 10) + lines_around_missing[phrase] = lines[context_start:context_end] + except Exception as e: + logger.error(f"Error reading log file {log_path}: {e}") + return False, phrases, {"error": [f"Error reading log file: {e}"]} + + return len(missing_phrases) == 0, missing_phrases, lines_around_missing + + +def output_validator( + log_file_path: str, error_keywords: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Simple validator that checks for error keywords in a log file. + + Args: + log_file_path: Path to the log file + error_keywords: List of keywords indicating errors + + Returns: + Dictionary with validation results: + { + 'is_pass': bool, + 'errors': list of error dictionaries, + 'phrase_mismatches': list of phrase mismatches (empty in this validator) + } + """ + is_pass, errors = check_for_errors(log_file_path, error_keywords) + + return { + "is_pass": is_pass, + "errors": errors, + "phrase_mismatches": [], # Not used in this simple validator + } diff --git a/tests/validation/common/visualisation/audio_graph.py b/tests/validation/common/visualisation/audio_graph.py index 09332a679..6fe53f3b0 100644 --- a/tests/validation/common/visualisation/audio_graph.py +++ b/tests/validation/common/visualisation/audio_graph.py @@ -416,7 +416,7 @@ def generate_waveform_plot( epilog=""" Example usage: python3 audio_graph.py /path/to/file1.pcm /path/to/file2.pcm --sample_rate 44100 --output_file output.png --num_channels1 1 --num_channels2 1 --downsample_factor 10 --start_time 0 --end_time 0.03 --pcm_format 16 - + For single file: python3 audio_graph.py /path/to/file1.pcm --sample_rate 44100 --output_file output.png --num_channels1 2 --downsample_factor 10 --pcm_format 16 """, diff --git a/tests/validation/configs/config_readme.md b/tests/validation/configs/config_readme.md index e84685ab2..a72eb484b 100644 --- a/tests/validation/configs/config_readme.md +++ b/tests/validation/configs/config_readme.md @@ -25,6 +25,13 @@ hosts: extra_info: mtl_path: /opt/intel/mtl nicctl_path: /opt/intel/mtl/script + filepath: /mnt/media/ + output_path: /home/gta/received/ + ffmpeg_path: /opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mcm_build/bin/ffmpeg + prefix_variables: + LD_LIBRARY_PATH: /opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mcm_build/lib + NO_PROXY: 127.0.0.1,localhost + no_proxy: 127.0.0.1,localhost media_proxy: st2110: true sdk_port: 8002 @@ -66,6 +73,12 @@ A list of host definitions. Each host can have the following fields: - **integrity_path**: Path for integrity scripts on the host if you want to run integrity tests (optional). - **mtl_path**: Custom path to the MTL repo (optional) default is /mcm_path/_build/mtl. - **nicctl_path**: Path to `nicctl.sh` script (optional). + - **filepath**: Path to input media files for transmitter (e.g., `/mnt/media/`). + - **output_path**: Path where output files will be stored (e.g., `/home/gta/received/`). + - **ffmpeg_path**: Path to FFmpeg binary (e.g., `/opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mcm_build/bin/ffmpeg`). + - **prefix_variables**: Environment variables to set before running FFmpeg. + - **LD_LIBRARY_PATH**: Path to FFmpeg libraries. + - **NO_PROXY**, **no_proxy**: Proxy bypass settings. - **media_proxy**: (Optional) Media proxy configuration. DO NOT set this if you don't want to run media proxy process. - **st2110**: Set to `true` to use the ST2110 bridge. - **sdk_port**: Port for media proxy SDK. @@ -85,6 +98,7 @@ A list of host definitions. Each host can have the following fields: - Fields marked as optional can be omitted if default values are sufficient. - You can add more hosts or network interfaces as needed. - If a field is not set, the system will use default values or those set in the OS. +- FFmpeg and file path configurations are now recommended to be defined directly in the host's `extra_info` section rather than using the previously nested `tx` and `rx` structure, which has been deprecated. --- @@ -92,23 +106,23 @@ A list of host definitions. Each host can have the following fields: ## Test config Structure Overview ```yaml -# Mesh agent configuration +# Mesh agent configuration (if not defined in topology) mesh-agent: control_port: 8100 proxy_port: 50051 -# ST2110 configuration +# ST2110 configuration (if not defined in topology) st2110: sdk: 8002 -# RDMA configuration +# RDMA configuration (if not defined in topology) rdma: rdma_ports: 9100-9999 -# Path configurations -media_path: "/mnt/media/" +# Global path configurations (if not defined in host's extra_info) +# Note: It's now recommended to define these in the host's extra_info section +# of the topology file for better organization and host-specific settings input_path: "/opt/intel/input_path/" -output_path: "/opt/intel/output_path/" # Build configurations mcm_ffmpeg_rebuild: false # Set to true to rebuild FFmpeg with MCM plugin @@ -171,13 +185,20 @@ Key path constants defined in `Engine/const.py`: - `INTEL_BASE_PATH = "/opt/intel"` - Base path for all Intel software - `MCM_PATH = "/opt/intel/mcm"` - Path to the MCM repository - `MTL_PATH = "/opt/intel/mtl"` - Path to the MTL repository +- `MCM_BUILD_PATH = "/opt/intel/_build/mcm"` - Path for MCM built binaries +- `MTL_BUILD_PATH = "/opt/intel/_build/mtl"` - Path for MTL built binaries - `DEFAULT_FFMPEG_PATH = "/opt/intel/ffmpeg"` - Path to the FFmpeg repository - `DEFAULT_OPENH264_PATH = "/opt/intel/openh264"` - Path to the OpenH264 installation -- `DEFAULT_MCM_FFMPEG_PATH` - Path to the MCM FFmpeg build -- `DEFAULT_MTL_FFMPEG_PATH` - Path to the MTL FFmpeg build +- `ALLOWED_FFMPEG_VERSIONS = ["6.1", "7.0"]` - Supported FFmpeg versions +- `DEFAULT_MCM_FFMPEG_VERSION = "7.0"` - Default FFmpeg version for MCM +- `DEFAULT_MCM_FFMPEG_PATH = "/opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mcm_build"` - Path to the MCM FFmpeg build +- `DEFAULT_MCM_FFMPEG_LD_LIBRARY_PATH = "/opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mcm_build/lib"` - Library path for MCM FFmpeg +- `DEFAULT_MTL_FFMPEG_VERSION = "7.0"` - Default FFmpeg version for MTL +- `DEFAULT_MTL_FFMPEG_PATH = "/opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mtl_build"` - Path to the MTL FFmpeg build +- `DEFAULT_MTL_FFMPEG_LD_LIBRARY_PATH = "/opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mtl_build/lib"` - Library path for MTL FFmpeg - `DEFAULT_MEDIA_PATH = "/mnt/media/"` - Path to the media files for testing -- `DEFAULT_INPUT_PATH = "/opt/intel/input_path/"` - Path for input files -- `DEFAULT_OUTPUT_PATH = "/opt/intel/output_path/"` - Path for output files +- `DEFAULT_INPUT_PATH = "/opt/intel/input_path"` - Path for input files +- `DEFAULT_OUTPUT_PATH = "/opt/intel/output_path"` - Path for output files ## Using the Test Configuration diff --git a/tests/validation/configs/test_config_workflow.yaml b/tests/validation/configs/test_config_workflow.yaml new file mode 100644 index 000000000..bdad2b126 --- /dev/null +++ b/tests/validation/configs/test_config_workflow.yaml @@ -0,0 +1,5 @@ +broadcast_ip: '239.2.39.239' +port: 20000 +payload_type: 100 +test_time_sec: 90 +log_path: {{ LOG_PATH }} \ No newline at end of file diff --git a/tests/validation/configs/topology_config_workflow.yaml b/tests/validation/configs/topology_config_workflow.yaml new file mode 100644 index 000000000..2704a13c4 --- /dev/null +++ b/tests/validation/configs/topology_config_workflow.yaml @@ -0,0 +1,38 @@ +--- +metadata: + version: '2.4' +hosts: + - name: mesh-agent + instantiate: true + role: sut + network_interfaces: + - pci_device: 8086:1592 + all_interfaces: true + connections: + - ip_address: 127.0.0.1 + connection_type: SSHConnection + connection_options: + port: 22 + username: {{ USER }} + password: None + key_path: {{ KEY_PATH }} + extra_info: + input_path: {{ INPUT_PATH }} + output_path: {{ OUTPUT_PATH }} + integrity_path: {{ INTEGRITY_PATH }} + mtl_path: {{ MTL_PATH }} + mcm_path: {{ MCM_PATH }} + mcm_ffmpeg_path: {{ MCM_FFMPEG_7_0 }} + mcm_prefix_variables: + LD_LIBRARY_PATH: {{ LD_LIBRARY_PATH }} + NO_PROXY: 127.0.0.1,localhost + no_proxy: 127.0.0.1,localhost + nicctl_path: {{ NICCTL_PATH }} + mesh_agent: + control_port: 8100 + proxy_port: 50051 + media_proxy: + sdk_port: 8002 + st2110: true + rdma: true + rdma_ports: '9100-9999' diff --git a/tests/validation/configs/topology_template.yaml b/tests/validation/configs/topology_template.yaml index 72439ec1c..9351c2f0e 100644 --- a/tests/validation/configs/topology_template.yaml +++ b/tests/validation/configs/topology_template.yaml @@ -18,6 +18,18 @@ hosts: extra_info: # This is the list of extra information that will be used in the test for each host mtl_path: /opt/intel/mtl # this is custom path to mtl repo if you want to use different path than default in mcm _build directory nicctl_path: /opt/intel/mtl/script # this is path for nicctl.sh script if you want to use different path + + # File paths for tests + filepath: /mnt/media/ # Path to input media files for transmitter + output_path: /home/gta/received/ # Path where output files will be stored + + # FFmpeg configuration + ffmpeg_path: /opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mcm_build/bin/ffmpeg # Path to FFmpeg binary + prefix_variables: # Environment variables to set before running FFmpeg + LD_LIBRARY_PATH: /opt/intel/_build/ffmpeg-7.0/ffmpeg-7-0_mcm_build/lib + NO_PROXY: 127.0.0.1,localhost + no_proxy: 127.0.0.1,localhost + media_proxy: # st2110 and rdma dev and ips are optional but remember that it overrites the default values and must be set in host st2110: {{ st2110|default(true) }} # set it to true if you want to use st2110 bridge for media proxy sdk_port: {{ sdk_port|default(8002) }} # this is the port that will be used for media proxy sdk diff --git a/tests/validation/conftest.py b/tests/validation/conftest.py index 71d21a8b0..e8abe368f 100644 --- a/tests/validation/conftest.py +++ b/tests/validation/conftest.py @@ -7,6 +7,7 @@ import shutil from ipaddress import IPv4Interface from pathlib import Path +from typing import Dict from mfd_host import Host from mfd_connect.exceptions import ( @@ -17,6 +18,7 @@ from common.mtl_manager.mtlManager import MtlManager from common.nicctl import Nicctl + from Engine.const import ( ALLOWED_FFMPEG_VERSIONS, DEFAULT_FFMPEG_PATH, @@ -29,15 +31,45 @@ DEFAULT_OPENH264_PATH, DEFAULT_OUTPUT_PATH, INTEL_BASE_PATH, - LOG_FOLDER, MCM_BUILD_PATH, MTL_BUILD_PATH, OPENH264_VERSION_TAG, + TESTCMD_LVL +) +from Engine.csv_report import ( + csv_add_test, + csv_write_report, + update_compliance_result, + +) +from Engine.mcm_apps import ( + MediaProxy, + MeshAgent, + get_mcm_path, + get_mtl_path, + get_log_folder_path, ) -from Engine.mcm_apps import MediaProxy, MeshAgent, get_mcm_path, get_mtl_path from datetime import datetime +from mfd_common_libs.custom_logger import add_logging_level +from mfd_common_libs.log_levels import TEST_FAIL, TEST_INFO, TEST_PASS +from pytest_mfd_logging.amber_log_formatter import AmberLogFormatter + +import re logger = logging.getLogger(__name__) +phase_report_key = pytest.StashKey[Dict[str, pytest.CollectReport]]() + + +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_makereport(item, call): + # execute all other hooks to obtain the report object + rep = yield + + # store test results for each phase of a call, which can + # be "setup", "call", "teardown" + item.stash.setdefault(phase_report_key, {})[rep.when] = rep + + return rep @pytest.fixture(scope="function") @@ -175,36 +207,58 @@ def media_path(test_config: dict) -> str: @pytest.fixture(scope="session") -def log_path_dir(test_config: dict) -> str: +def log_path_dir(test_config: dict, pytestconfig): + """ + Creates and returns the main log directory path for the test session. + + The directory is created under the path provided by get_log_folder_path. + If keep_logs is False, the existing log directory is removed before creating a new one. + + :param test_config: Dictionary containing test configuration. + """ + add_logging_level("TESTCMD", TESTCMD_LVL) keep_logs = test_config.get("keep_logs", True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_dir_name = f"log_{timestamp}" - validation_dir = Path(__file__).parent - log_path = test_config.get("log_path") - log_dir = Path(log_path) if log_path else Path(validation_dir, LOG_FOLDER) + log_dir = get_log_folder_path(test_config) if log_dir.exists() and not keep_logs: shutil.rmtree(log_dir) log_dir = Path(log_dir, log_dir_name) log_dir.mkdir(parents=True, exist_ok=True) - return str(log_dir) + yield str(log_dir) + pytest_log = Path(pytestconfig.inicfg["log_file"]) + shutil.copy(str(pytest_log), log_dir / pytest_log.name) + csv_write_report(str(log_dir / "report.csv")) + + +def sanitize_name(test_name): + """ + Sanitizes a test name by replacing invalid characters with underscores, + collapsing multiple underscores into one, and removing leading/trailing underscores. + """ + sanitized_name = re.sub(r'[|<>:"*?\r\n\[\]]| = |_{2,}', "_", test_name) + sanitized_name = re.sub(r"_{2,}", "_", sanitized_name).strip("_") + return sanitized_name @pytest.fixture(scope="function") -def log_path(log_path_dir: str, request) -> str: +def log_path(log_path_dir: str, request) -> Path: """ Create a test-specific subdirectory within the main log directory. + Sanitizes test names to ensure valid directory names by replacing invalid characters. - :param log_path: The main log directory path from the log_path fixture. - :type log_path: str + :param log_path_dir: The main log directory path from the log_path_dir fixture. + :type log_path_dir: str :param request: Pytest request object to get test name. :return: Path to test-specific log subdirectory. :rtype: str """ test_name = request.node.name - test_log_path = Path(log_path_dir, test_name) + sanitized_name = sanitize_name(test_name) + test_log_path = Path(log_path_dir, sanitized_name) test_log_path.mkdir(parents=True, exist_ok=True) - return str(test_log_path) + return Path(test_log_path) @pytest.fixture(scope="session") @@ -300,22 +354,19 @@ def mtl_manager(hosts): @pytest.fixture(scope="session", autouse=True) def cleanup_processes(hosts: dict) -> None: """ - Kills mesh-agent, media_proxy, ffmpeg, and all Rx*App and Tx*App processes on all hosts before running the tests. + Kill mesh-agent, media_proxy, ffmpeg, and all Rx*App/Tx*App processes on all hosts before running tests. """ for host in hosts.values(): + connection = host.connection for proc in ["mesh-agent", "media_proxy", "ffmpeg"]: try: - connection = host.connection - # connection.enable_sudo() connection.execute_command(f"pgrep {proc}", stderr_to_stdout=True) connection.execute_command(f"pkill -9 {proc}", stderr_to_stdout=True) except Exception as e: - logger.warning(f"Failed to check/kill {proc} on {host.name}: {e}") - # Kill all Rx*App and Tx*App processes (e.g., RxVideoApp, RxAudioApp, RxBlobApp, TxVideoApp, etc.) + if not (hasattr(e, "returncode") and e.returncode == 1): + logger.warning(f"Failed to check/kill {proc} on {host.name}: {e}") for pattern in ["^Rx[A-Za-z]+App$", "^Tx[A-Za-z]+App$"]: try: - connection = host.connection - # connection.enable_sudo() connection.execute_command( f"pgrep -f '{pattern}'", stderr_to_stdout=True ) @@ -323,9 +374,10 @@ def cleanup_processes(hosts: dict) -> None: f"pkill -9 -f '{pattern}'", stderr_to_stdout=True ) except Exception as e: - logger.warning( - f"Failed to check/kill processes matching {pattern} on {host.name}: {e}" - ) + if not (hasattr(e, "returncode") and e.returncode == 1): + logger.warning( + f"Failed to check/kill processes matching {pattern} on {host.name}: {e}" + ) logger.info("Cleanup of processes completed.") @@ -787,3 +839,35 @@ def log_interface_driver_info(hosts: dict[str, Host]) -> None: logger.info( f"Interface {interface.name} on host {host.name} uses driver: {driver_info.driver_name} ({driver_info.driver_version})" ) + + +@pytest.fixture(scope="function", autouse=True) +def log_case(request, caplog: pytest.LogCaptureFixture): + case_id = request.node.nodeid + yield + report = request.node.stash[phase_report_key] + if report["setup"].failed: + logging.log(level=TEST_FAIL, msg=f"Setup failed for {case_id}") + result = "Fail" + elif ("call" not in report) or report["call"].failed: + logging.log(level=TEST_FAIL, msg=f"Test failed for {case_id}") + result = "Fail" + elif report["call"].passed: + logging.log(level=TEST_PASS, msg=f"Test passed for {case_id}") + result = "Pass" + else: + logging.log(level=TEST_INFO, msg=f"Test skipped for {case_id}") + result = "Skip" + + commands = [] + for record in caplog.get_records("call"): + if record.levelno == TESTCMD_LVL: + commands.append(record.message) + + csv_add_test( + test_case=case_id, + commands="\n".join(commands), + result=result, + issue="n/a", + result_note="n/a", + ) diff --git a/tests/validation/functional/cluster/audio/test_ffmpeg_audio.py b/tests/validation/functional/cluster/audio/test_ffmpeg_audio.py index 9b29cef98..346b6c6a4 100644 --- a/tests/validation/functional/cluster/audio/test_ffmpeg_audio.py +++ b/tests/validation/functional/cluster/audio/test_ffmpeg_audio.py @@ -23,7 +23,9 @@ @pytest.mark.parametrize("audio_type", [file for file in audio_files.keys()]) -def test_cluster_ffmpeg_audio(hosts, media_proxy, test_config, audio_type: str) -> None: +def test_cluster_ffmpeg_audio( + hosts, media_proxy, test_config, audio_type: str, log_path +) -> None: # Get TX and RX hosts host_list = list(hosts.values()) if len(host_list) < 2: @@ -75,7 +77,9 @@ def test_cluster_ffmpeg_audio(hosts, media_proxy, test_config, audio_type: str) ) logger.debug(f"Tx command on {tx_host.name}: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # >>>>> MCM Rx mcm_rx_inp = FFmpegMcmMultipointGroupAudioIO( @@ -100,7 +104,9 @@ def test_cluster_ffmpeg_audio(hosts, media_proxy, test_config, audio_type: str) ) logger.debug(f"Rx command on {rx_host.name}: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) mcm_rx_executor.start() mcm_tx_executor.start() diff --git a/tests/validation/functional/cluster/video/test_ffmpeg_video.py b/tests/validation/functional/cluster/video/test_ffmpeg_video.py index ec26a0d74..0ab9e2cff 100644 --- a/tests/validation/functional/cluster/video/test_ffmpeg_video.py +++ b/tests/validation/functional/cluster/video/test_ffmpeg_video.py @@ -20,7 +20,9 @@ @pytest.mark.parametrize("video_type", [k for k in yuv_files.keys()]) -def test_cluster_ffmpeg_video(hosts, media_proxy, test_config, video_type: str) -> None: +def test_cluster_ffmpeg_video( + hosts, media_proxy, test_config, video_type: str, log_path +) -> None: # Get TX and RX hosts host_list = list(hosts.values()) if len(host_list) < 2: @@ -68,7 +70,9 @@ def test_cluster_ffmpeg_video(hosts, media_proxy, test_config, video_type: str) ) logger.debug(f"Tx command on {tx_host.name}: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # >>>>> MCM Rx mcm_rx_inp = FFmpegMcmMultipointGroupVideoIO( @@ -95,7 +99,9 @@ def test_cluster_ffmpeg_video(hosts, media_proxy, test_config, video_type: str) ) logger.debug(f"Rx command on {rx_host.name}: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) mcm_rx_executor.start() mcm_tx_executor.start() diff --git a/tests/validation/functional/local/audio/test_audio.py b/tests/validation/functional/local/audio/test_audio.py index 6d8226fb6..f6edbe4ec 100644 --- a/tests/validation/functional/local/audio/test_audio.py +++ b/tests/validation/functional/local/audio/test_audio.py @@ -9,7 +9,11 @@ import Engine.rx_tx_app_connection import Engine.rx_tx_app_engine_mcm as utils import Engine.rx_tx_app_payload -from Engine.const import DEFAULT_LOOP_COUNT, MCM_ESTABLISH_TIMEOUT +from Engine.const import ( + DEFAULT_LOOP_COUNT, + MCM_ESTABLISH_TIMEOUT, + MCM_RXTXAPP_RUN_TIMEOUT, +) from Engine.media_files import audio_files logger = logging.getLogger(__name__) @@ -57,7 +61,9 @@ def test_audio(build_TestApp, hosts, media_proxy, media_path, file, log_path) -> tx_executor.stop() rx_executor.stop() + # TODO add validate() function to check if the output file is correct + + rx_executor.cleanup() + assert tx_executor.is_pass is True, "TX process did not pass" assert rx_executor.is_pass is True, "RX process did not pass" - - # TODO add validate() function to check if the output file is correct diff --git a/tests/validation/functional/local/audio/test_audio_25_03.py b/tests/validation/functional/local/audio/test_audio_25_03.py index 60034d568..8e81e44eb 100644 --- a/tests/validation/functional/local/audio/test_audio_25_03.py +++ b/tests/validation/functional/local/audio/test_audio_25_03.py @@ -17,7 +17,13 @@ from Engine.media_files import audio_files_25_03 -@pytest.mark.parametrize("file", audio_files_25_03.keys()) +@pytest.mark.parametrize( + "file", + [ + pytest.param("PCM16_48000_Mono", marks=pytest.mark.smoke), + *[f for f in audio_files_25_03.keys() if f != "PCM16_48000_Mono"], + ], +) def test_audio_25_03( build_TestApp, hosts, media_proxy, media_path, file, log_path ) -> None: @@ -61,7 +67,9 @@ def test_audio_25_03( tx_executor.stop() rx_executor.stop() + # TODO add validate() function to check if the output file is correct + + rx_executor.cleanup() + assert tx_executor.is_pass is True, "TX process did not pass" assert rx_executor.is_pass is True, "RX process did not pass" - - # TODO add validate() function to check if the output file is correct diff --git a/tests/validation/functional/local/audio/test_ffmpeg_audio.py b/tests/validation/functional/local/audio/test_ffmpeg_audio.py index f458e003e..39423e303 100644 --- a/tests/validation/functional/local/audio/test_ffmpeg_audio.py +++ b/tests/validation/functional/local/audio/test_ffmpeg_audio.py @@ -7,7 +7,7 @@ import pytest import logging -from ....Engine.media_files import audio_files +from Engine.media_files import audio_files_25_03 from common.ffmpeg_handler.ffmpeg import FFmpeg, FFmpegExecutor from common.ffmpeg_handler.ffmpeg_enums import ( @@ -17,92 +17,127 @@ from common.ffmpeg_handler.ffmpeg_io import FFmpegAudioIO from common.ffmpeg_handler.mcm_ffmpeg import FFmpegMcmMemifAudioIO +from Engine.const import ( + FFMPEG_RUN_TIMEOUT, + DEFAULT_OUTPUT_PATH, +) logger = logging.getLogger(__name__) -@pytest.mark.parametrize("audio_type", [k for k in audio_files.keys()]) -def test_local_ffmpeg_audio(media_proxy, hosts, test_config, audio_type: str) -> None: - # media_proxy fixture used only to ensure that the media proxy is running - # Get TX and RX hosts +@pytest.mark.usefixtures("media_proxy") +@pytest.mark.parametrize( + "audio_type", + [ + pytest.param("PCM16_48000_Stereo", marks=pytest.mark.smoke), + *[f for f in audio_files_25_03.keys() if f != "PCM16_48000_Stereo"], + ], +) +def test_local_ffmpeg_audio( + hosts, test_config, audio_type: str, log_path, media_path +) -> None: host_list = list(hosts.values()) if len(host_list) < 1: pytest.skip("Local tests require at least 1 host") tx_host = rx_host = host_list[0] - tx_prefix_variables = test_config["tx"].get("prefix_variables", None) - rx_prefix_variables = test_config["rx"].get("prefix_variables", None) - tx_prefix_variables["MCM_MEDIA_PROXY_PORT"] = ( - tx_host.topology.extra_info.media_proxy["sdk_port"] - ) - rx_prefix_variables["MCM_MEDIA_PROXY_PORT"] = ( - rx_host.topology.extra_info.media_proxy["sdk_port"] - ) + + if hasattr(tx_host.topology.extra_info, "mcm_prefix_variables"): + prefix_variables = dict(tx_host.topology.extra_info.mcm_prefix_variables) + else: + prefix_variables = {} + prefix_variables["MCM_MEDIA_PROXY_PORT"] = tx_host.topology.extra_info.media_proxy[ + "sdk_port" + ] + + # PCM 8 (pcm_s8) is not supported by the MCM FFmpeg plugin. Skip those cases. + if audio_files_25_03[audio_type]["format"] == "pcm_s8": + pytest.skip( + "PCM 8 is not supported by Media Communications Mesh FFmpeg plugin!" + ) audio_format = audio_file_format_to_format_dict( - str(audio_files[audio_type]["format"]) + str(audio_files_25_03[audio_type]["format"]) ) # audio format - audio_channel_layout = audio_files[audio_type].get( + audio_channel_layout = audio_files_25_03[audio_type].get( "channel_layout", - audio_channel_number_to_layout(int(audio_files[audio_type]["channels"])), + audio_channel_number_to_layout(int(audio_files_25_03[audio_type]["channels"])), ) - if audio_files[audio_type]["sample_rate"] not in [48000, 44100, 96000]: + if audio_files_25_03[audio_type]["sample_rate"] not in [48000, 44100, 96000]: raise Exception( - f"Not expected audio sample rate of {audio_files[audio_type]['sample_rate']}!" + f"Not expected audio sample rate of {audio_files_25_03[audio_type]['sample_rate']}!" ) # >>>>> MCM Tx mcm_tx_inp = FFmpegAudioIO( f=audio_format["ffmpeg_f"], - ac=int(audio_files[audio_type]["channels"]), - ar=int(audio_files[audio_type]["sample_rate"]), + ac=int(audio_files_25_03[audio_type]["channels"]), + ar=int(audio_files_25_03[audio_type]["sample_rate"]), stream_loop=False, - input_path=f'{test_config["tx"]["filepath"]}{audio_files[audio_type]["filename"]}', + input_path=f'{media_path}{audio_files_25_03[audio_type]["filename"]}', ) mcm_tx_outp = FFmpegMcmMemifAudioIO( - channels=int(audio_files[audio_type]["channels"]), - sample_rate=int(audio_files[audio_type]["sample_rate"]), + channels=int(audio_files_25_03[audio_type]["channels"]), + sample_rate=int(audio_files_25_03[audio_type]["sample_rate"]), f=audio_format["mcm_f"], output_path="-", ) mcm_tx_ff = FFmpeg( - prefix_variables=tx_prefix_variables, - ffmpeg_path=test_config["tx"]["ffmpeg_path"], + prefix_variables=prefix_variables, + ffmpeg_path=tx_host.topology.extra_info.mcm_ffmpeg_path, ffmpeg_input=mcm_tx_inp, ffmpeg_output=mcm_tx_outp, yes_overwrite=False, ) - logger.debug(f"Tx command: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # >>>>> MCM Rx mcm_rx_inp = FFmpegMcmMemifAudioIO( - channels=int(audio_files[audio_type]["channels"]), - sample_rate=int(audio_files[audio_type]["sample_rate"]), + channels=int(audio_files_25_03[audio_type]["channels"]), + sample_rate=int(audio_files_25_03[audio_type]["sample_rate"]), f=audio_format["mcm_f"], input_path="-", ) mcm_rx_outp = FFmpegAudioIO( f=audio_format["ffmpeg_f"], - ac=int(audio_files[audio_type]["channels"]), - ar=int(audio_files[audio_type]["sample_rate"]), + ac=int(audio_files_25_03[audio_type]["channels"]), + ar=int(audio_files_25_03[audio_type]["sample_rate"]), channel_layout=audio_channel_layout, - output_path=f'{test_config["rx"]["filepath"]}test_{audio_files[audio_type]["filename"]}', + output_path=f'{getattr(rx_host.topology.extra_info, "output_path", DEFAULT_OUTPUT_PATH)}/test_{audio_files_25_03[audio_type]["filename"]}', ) mcm_rx_ff = FFmpeg( - prefix_variables=rx_prefix_variables, - ffmpeg_path=test_config["rx"]["ffmpeg_path"], + prefix_variables=prefix_variables, + ffmpeg_path=rx_host.topology.extra_info.mcm_ffmpeg_path, ffmpeg_input=mcm_rx_inp, ffmpeg_output=mcm_rx_outp, yes_overwrite=True, ) logger.debug(f"Rx command: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) mcm_rx_executor.start() mcm_tx_executor.start() + try: + mcm_rx_executor.wait_with_timeout(timeout=FFMPEG_RUN_TIMEOUT) + except Exception as e: + logging.warning(f"RX executor did not finish in time or error occurred: {e}") + mcm_rx_executor.stop(wait=test_config.get("test_time_sec", 0.0)) mcm_tx_executor.stop(wait=test_config.get("test_time_sec", 0.0)) + + mcm_rx_executor.validate() + mcm_tx_executor.validate() + + # TODO add validate() function to check if the output file is correct + + mcm_rx_executor.cleanup() + + assert mcm_tx_executor.is_pass is True, "TX FFmpeg process did not pass" + assert mcm_rx_executor.is_pass is True, "RX FFmpeg process did not pass" diff --git a/tests/validation/functional/local/blob/test_blob.py b/tests/validation/functional/local/blob/test_blob.py index aacaa392b..e98e69e46 100644 --- a/tests/validation/functional/local/blob/test_blob.py +++ b/tests/validation/functional/local/blob/test_blob.py @@ -58,7 +58,9 @@ def test_blob(build_TestApp, hosts, media_proxy, media_path, file, log_path) -> tx_executor.stop() rx_executor.stop() + # TODO add validate() function to check if the output file is correct + + rx_executor.cleanup() + assert tx_executor.is_pass is True, "TX process did not pass" assert rx_executor.is_pass is True, "RX process did not pass" - - # TODO add validate() function to check if the output file is correct diff --git a/tests/validation/functional/local/blob/test_blob_25_03.py b/tests/validation/functional/local/blob/test_blob_25_03.py index c64d0dd50..385b40fe5 100644 --- a/tests/validation/functional/local/blob/test_blob_25_03.py +++ b/tests/validation/functional/local/blob/test_blob_25_03.py @@ -18,6 +18,7 @@ from Engine.media_files import blob_files_25_03 +@pytest.mark.smoke @pytest.mark.parametrize("file", [file for file in blob_files_25_03.keys()]) def test_blob_25_03( build_TestApp, hosts, media_proxy, media_path, file, log_path @@ -62,7 +63,9 @@ def test_blob_25_03( tx_executor.stop() rx_executor.stop() + # TODO add validate() function to check if the output file is correct + + rx_executor.cleanup() + assert tx_executor.is_pass is True, "TX process did not pass" assert rx_executor.is_pass is True, "RX process did not pass" - - # TODO add validate() function to check if the output file is correct diff --git a/tests/validation/functional/local/video/test_ffmpeg_video.py b/tests/validation/functional/local/video/test_ffmpeg_video.py index fdb6c2c5a..203bedbc4 100644 --- a/tests/validation/functional/local/video/test_ffmpeg_video.py +++ b/tests/validation/functional/local/video/test_ffmpeg_video.py @@ -7,7 +7,7 @@ import pytest import logging -from ....Engine.media_files import yuv_files +from Engine.media_files import video_files_25_03 from common.ffmpeg_handler.ffmpeg import FFmpeg, FFmpegExecutor from common.ffmpeg_handler.ffmpeg_enums import ( @@ -17,20 +17,39 @@ from common.ffmpeg_handler.ffmpeg_io import FFmpegVideoIO from common.ffmpeg_handler.mcm_ffmpeg import FFmpegMcmMemifVideoIO +from Engine.const import ( + FFMPEG_RUN_TIMEOUT, + DEFAULT_OUTPUT_PATH, +) logger = logging.getLogger(__name__) -@pytest.mark.parametrize("video_type", [k for k in yuv_files.keys()]) -def test_local_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) -> None: - # media_proxy fixture used only to ensure that the media proxy is running - # Get TX and RX hosts +@pytest.mark.usefixtures("media_proxy") +@pytest.mark.parametrize( + "file", + [ + pytest.param("FullHD_59.94", marks=pytest.mark.smoke), + *[f for f in video_files_25_03.keys() if f != "FullHD_59.94"], + ], +) +def test_local_ffmpeg_video( + hosts, test_config, file: str, log_path, media_path +) -> None: host_list = list(hosts.values()) if len(host_list) < 1: pytest.skip("Local tests require at least 1 host") tx_host = rx_host = host_list[0] - tx_prefix_variables = test_config["tx"].get("prefix_variables", None) - rx_prefix_variables = test_config["rx"].get("prefix_variables", None) + + if hasattr(tx_host.topology.extra_info, "mcm_prefix_variables"): + tx_prefix_variables = dict(tx_host.topology.extra_info.mcm_prefix_variables) + else: + tx_prefix_variables = {} + if hasattr(rx_host.topology.extra_info, "mcm_prefix_variables"): + rx_prefix_variables = dict(rx_host.topology.extra_info.mcm_prefix_variables) + else: + rx_prefix_variables = {} + tx_prefix_variables["MCM_MEDIA_PROXY_PORT"] = ( tx_host.topology.extra_info.media_proxy["sdk_port"] ) @@ -38,10 +57,12 @@ def test_local_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) -> rx_host.topology.extra_info.media_proxy["sdk_port"] ) - frame_rate = str(yuv_files[video_type]["fps"]) - video_size = f'{yuv_files[video_type]["width"]}x{yuv_files[video_type]["height"]}' + frame_rate = str(video_files_25_03[file]["fps"]) + video_size = ( + f'{video_files_25_03[file]["width"]}x{video_files_25_03[file]["height"]}' + ) pixel_format = video_file_format_to_payload_format( - str(yuv_files[video_type]["file_format"]) + str(video_files_25_03[file]["file_format"]) ) conn_type = McmConnectionType.mpg.value @@ -51,7 +72,7 @@ def test_local_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) -> video_size=video_size, pixel_format=pixel_format, stream_loop=False, - input_path=f'{test_config["tx"]["filepath"]}{yuv_files[video_type]["filename"]}', + input_path=f'{media_path}{video_files_25_03[file]["filename"]}', ) mcm_tx_outp = FFmpegMcmMemifVideoIO( f="mcm", @@ -63,14 +84,16 @@ def test_local_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) -> ) mcm_tx_ff = FFmpeg( prefix_variables=tx_prefix_variables, - ffmpeg_path=test_config["tx"]["ffmpeg_path"], + ffmpeg_path=tx_host.topology.extra_info.mcm_ffmpeg_path, ffmpeg_input=mcm_tx_inp, ffmpeg_output=mcm_tx_outp, yes_overwrite=False, ) logger.debug(f"Tx command: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # >>>>> MCM Rx mcm_rx_inp = FFmpegMcmMemifVideoIO( @@ -86,20 +109,37 @@ def test_local_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) -> framerate=frame_rate, video_size=video_size, pixel_format=pixel_format, - output_path=f'{test_config["rx"]["filepath"]}test_{yuv_files[video_type]["filename"]}', + output_path=f'{getattr(rx_host.topology.extra_info, "output_path", DEFAULT_OUTPUT_PATH)}/test_{video_files_25_03[file]["filename"]}', ) mcm_rx_ff = FFmpeg( prefix_variables=rx_prefix_variables, - ffmpeg_path=test_config["rx"]["ffmpeg_path"], + ffmpeg_path=rx_host.topology.extra_info.mcm_ffmpeg_path, ffmpeg_input=mcm_rx_inp, ffmpeg_output=mcm_rx_outp, yes_overwrite=True, ) logger.debug(f"Rx command: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) mcm_rx_executor.start() mcm_tx_executor.start() + try: + mcm_rx_executor.wait_with_timeout(timeout=FFMPEG_RUN_TIMEOUT) + except Exception as e: + logging.warning(f"RX executor did not finish in time or error occurred: {e}") + mcm_rx_executor.stop(wait=test_config.get("test_time_sec", 0.0)) mcm_tx_executor.stop(wait=test_config.get("test_time_sec", 0.0)) + + mcm_rx_executor.validate() + mcm_tx_executor.validate() + + # TODO add validate() function to check if the output file is correct + + mcm_rx_executor.cleanup() + + assert mcm_tx_executor.is_pass is True, "TX FFmpeg process did not pass" + assert mcm_rx_executor.is_pass is True, "RX FFmpeg process did not pass" diff --git a/tests/validation/functional/local/video/test_video.py b/tests/validation/functional/local/video/test_video.py index 0ea45f99a..de274d12d 100644 --- a/tests/validation/functional/local/video/test_video.py +++ b/tests/validation/functional/local/video/test_video.py @@ -10,7 +10,11 @@ import Engine.rx_tx_app_connection import Engine.rx_tx_app_engine_mcm as utils import Engine.rx_tx_app_payload -from Engine.const import DEFAULT_LOOP_COUNT, MCM_ESTABLISH_TIMEOUT +from Engine.const import ( + DEFAULT_LOOP_COUNT, + MCM_ESTABLISH_TIMEOUT, + MCM_RXTXAPP_RUN_TIMEOUT, +) from Engine.media_files import yuv_files logger = logging.getLogger(__name__) @@ -58,7 +62,9 @@ def test_video(build_TestApp, hosts, media_proxy, media_path, file, log_path) -> tx_executor.stop() rx_executor.stop() + # TODO add validate() function to check if the output file is correct + + rx_executor.cleanup() + assert tx_executor.is_pass is True, "TX process did not pass" assert rx_executor.is_pass is True, "RX process did not pass" - - # TODO add validate() function to check if the output file is correct diff --git a/tests/validation/functional/local/video/test_video_25_03.py b/tests/validation/functional/local/video/test_video_25_03.py index 600efcc4f..d5d868b79 100644 --- a/tests/validation/functional/local/video/test_video_25_03.py +++ b/tests/validation/functional/local/video/test_video_25_03.py @@ -18,7 +18,13 @@ from Engine.media_files import video_files_25_03 -@pytest.mark.parametrize("file", [file for file in video_files_25_03.keys()]) +@pytest.mark.parametrize( + "file", + [ + pytest.param("FullHD_60", marks=pytest.mark.smoke), + *[f for f in video_files_25_03.keys() if f != "FullHD_60"], + ], +) def test_video_25_03( build_TestApp, hosts, media_proxy, media_path, file, log_path ) -> None: @@ -62,7 +68,9 @@ def test_video_25_03( tx_executor.stop() rx_executor.stop() + # TODO add validate() function to check if the output file is correct + + rx_executor.cleanup() + assert tx_executor.is_pass is True, "TX process did not pass" assert rx_executor.is_pass is True, "RX process did not pass" - - # TODO add validate() function to check if the output file is correct diff --git a/tests/validation/functional/st2110/st20/test_3_2_st2110_standalone_video.py b/tests/validation/functional/st2110/st20/test_3_2_st2110_standalone_video.py index fe8db755c..b98b960d0 100644 --- a/tests/validation/functional/st2110/st20/test_3_2_st2110_standalone_video.py +++ b/tests/validation/functional/st2110/st20/test_3_2_st2110_standalone_video.py @@ -94,7 +94,7 @@ def test_3_2_st2110_standalone_video(hosts, test_config, video_type, log_path): ffmpeg_output=rx_ffmpeg_output, yes_overwrite=True, ) - rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=rx_ffmpeg) + rx_executor = FFmpegExecutor(rx_host, log_path=log_path, ffmpeg_instance=rx_ffmpeg) tx_executor.start() sleep(MCM_ESTABLISH_TIMEOUT) diff --git a/tests/validation/functional/st2110/st20/test_6_1_st2110_ffmpeg_video.py b/tests/validation/functional/st2110/st20/test_6_1_st2110_ffmpeg_video.py index 6b825e229..c8040e81d 100644 --- a/tests/validation/functional/st2110/st20/test_6_1_st2110_ffmpeg_video.py +++ b/tests/validation/functional/st2110/st20/test_6_1_st2110_ffmpeg_video.py @@ -19,7 +19,7 @@ @pytest.mark.usefixtures("media_proxy") @pytest.mark.parametrize("video_file", video_files) -def test_6_1_st2110_ffmpeg_video(hosts, test_config, video_file): +def test_6_1_st2110_ffmpeg_video(hosts, test_config, video_file, log_path): video_file = video_files[video_file] tx_host = hosts["mesh-agent"] @@ -76,6 +76,7 @@ def test_6_1_st2110_ffmpeg_video(hosts, test_config, video_file): mtl_tx_ffmpeg_executor = FFmpegExecutor( host=tx_host, ffmpeg_instance=mtl_tx_ffmpeg, + log_path=log_path, ) # Host A --- MCM FFmpeg Rx @@ -115,6 +116,7 @@ def test_6_1_st2110_ffmpeg_video(hosts, test_config, video_file): mcm_rx_a_ffmpeg_executor = FFmpegExecutor( host=rx_a_host, ffmpeg_instance=mcm_rx_a_ffmpeg, + log_path=log_path, ) # Host B --- MCM FFmpeg Rx @@ -154,6 +156,7 @@ def test_6_1_st2110_ffmpeg_video(hosts, test_config, video_file): mcm_rx_b_ffmpeg_executor = FFmpegExecutor( host=rx_b_host, ffmpeg_instance=mcm_rx_b_ffmpeg, + log_path=log_path, ) mtl_tx_ffmpeg_executor.start() diff --git a/tests/validation/functional/st2110/st20/test_ffmpeg_mcm_to_mtl_video.py b/tests/validation/functional/st2110/st20/test_ffmpeg_mcm_to_mtl_video.py index 1dd5e7287..9f4d96599 100644 --- a/tests/validation/functional/st2110/st20/test_ffmpeg_mcm_to_mtl_video.py +++ b/tests/validation/functional/st2110/st20/test_ffmpeg_mcm_to_mtl_video.py @@ -29,7 +29,7 @@ @pytest.mark.parametrize("video_type", [k for k in yuv_files.keys()]) def test_st2110_ffmpeg_mcm_to_mtl_video( - media_proxy, hosts, test_config, video_type: str + media_proxy, hosts, test_config, video_type: str, log_path ) -> None: # media_proxy fixture used only to ensure that the media proxy is running # Get TX and RX hosts @@ -94,7 +94,9 @@ def test_st2110_ffmpeg_mcm_to_mtl_video( yes_overwrite=False, ) logger.debug(f"Tx command executed on {tx_host.name}: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # MTL Rx mtl_rx_inp = FFmpegMtlSt20pRx( @@ -136,7 +138,9 @@ def test_st2110_ffmpeg_mcm_to_mtl_video( yes_overwrite=True, ) logger.debug(f"Rx command executed on {rx_host.name}: {mtl_rx_ff.get_command()}") - mtl_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mtl_rx_ff) + mtl_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mtl_rx_ff + ) time.sleep(2) # wait for media_proxy to start mtl_rx_executor.start() diff --git a/tests/validation/functional/st2110/st20/test_ffmpeg_mtl_to_mcm_video.py b/tests/validation/functional/st2110/st20/test_ffmpeg_mtl_to_mcm_video.py index 3db28eb1e..b923f16d1 100644 --- a/tests/validation/functional/st2110/st20/test_ffmpeg_mtl_to_mcm_video.py +++ b/tests/validation/functional/st2110/st20/test_ffmpeg_mtl_to_mcm_video.py @@ -32,7 +32,9 @@ @pytest.mark.parametrize("video_type", [k for k in yuv_files.keys()]) -def test_st2110_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) -> None: +def test_st2110_ffmpeg_video( + media_proxy, hosts, test_config, video_type: str, log_path +) -> None: # media_proxy fixture used only to ensure that the media proxy is running # Get TX and RX hosts host_list = list(hosts.values()) @@ -97,7 +99,9 @@ def test_st2110_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) - yes_overwrite=False, ) logger.debug(f"Tx command executed on {tx_host.name}: {mtl_tx_ff.get_command()}") - mtl_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mtl_tx_ff) + mtl_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mtl_tx_ff + ) # MCM Rx mcm_rx_inp = FFmpegMcmST2110VideoRx( @@ -129,7 +133,9 @@ def test_st2110_ffmpeg_video(media_proxy, hosts, test_config, video_type: str) - yes_overwrite=True, ) logger.debug(f"Rx command executed on {rx_host.name}: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) time.sleep(2) # wait for media_proxy to start mcm_rx_executor.start() diff --git a/tests/validation/functional/st2110/st30/test_3_2_st2110_standalone_audio.py b/tests/validation/functional/st2110/st30/test_3_2_st2110_standalone_audio.py index e35714b7f..5b938afa9 100644 --- a/tests/validation/functional/st2110/st30/test_3_2_st2110_standalone_audio.py +++ b/tests/validation/functional/st2110/st30/test_3_2_st2110_standalone_audio.py @@ -107,7 +107,7 @@ def test_3_2_st2110_standalone_audio(hosts, test_config, audio_type, log_path): ffmpeg_output=rx_ffmpeg_output, yes_overwrite=True, ) - rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=rx_ffmpeg) + rx_executor = FFmpegExecutor(rx_host, log_path=log_path, ffmpeg_instance=rx_ffmpeg) tx_executor.start() sleep(MCM_ESTABLISH_TIMEOUT) diff --git a/tests/validation/functional/st2110/st30/test_6_1_st2110_ffmpeg_audio.py b/tests/validation/functional/st2110/st30/test_6_1_st2110_ffmpeg_audio.py index 402e0b57c..45ba18339 100644 --- a/tests/validation/functional/st2110/st30/test_6_1_st2110_ffmpeg_audio.py +++ b/tests/validation/functional/st2110/st30/test_6_1_st2110_ffmpeg_audio.py @@ -19,7 +19,7 @@ @pytest.mark.usefixtures("media_proxy") @pytest.mark.parametrize("audio_file", audio_files) -def test_6_1_st2110_ffmpeg_audio(hosts, test_config, audio_file): +def test_6_1_st2110_ffmpeg_audio(hosts, test_config, audio_file, log_path): audio_file = audio_files[audio_file] tx_host = hosts["mesh-agent"] @@ -85,6 +85,7 @@ def test_6_1_st2110_ffmpeg_audio(hosts, test_config, audio_file): mtl_tx_ffmpeg_executor = FFmpegExecutor( host=tx_host, ffmpeg_instance=mtl_tx_ffmpeg, + log_path=log_path, ) # Host A --- MCM FFmpeg Rx @@ -122,6 +123,7 @@ def test_6_1_st2110_ffmpeg_audio(hosts, test_config, audio_file): mcm_rx_a_ffmpeg_executor = FFmpegExecutor( host=rx_a_host, ffmpeg_instance=mcm_rx_a_ffmpeg, + log_path=log_path, ) # Host B --- MCM FFmpeg Rx @@ -159,6 +161,7 @@ def test_6_1_st2110_ffmpeg_audio(hosts, test_config, audio_file): mcm_rx_b_ffmpeg_executor = FFmpegExecutor( host=rx_b_host, ffmpeg_instance=mcm_rx_b_ffmpeg, + log_path=log_path, ) mtl_tx_ffmpeg_executor.start() diff --git a/tests/validation/functional/st2110/st30/test_ffmpeg_mcm_to_mtl_audio.py b/tests/validation/functional/st2110/st30/test_ffmpeg_mcm_to_mtl_audio.py index d50185f36..b045d90e1 100644 --- a/tests/validation/functional/st2110/st30/test_ffmpeg_mcm_to_mtl_audio.py +++ b/tests/validation/functional/st2110/st30/test_ffmpeg_mcm_to_mtl_audio.py @@ -30,7 +30,7 @@ @pytest.mark.parametrize("audio_type", [k for k in audio_files.keys()]) def test_st2110_ffmpeg_mcm_to_mtl_audio( - media_proxy, hosts, test_config, audio_type: str + media_proxy, hosts, test_config, audio_type: str, log_path ) -> None: # media_proxy fixture used only to ensure that the media proxy is running # Get TX and RX hosts @@ -109,7 +109,9 @@ def test_st2110_ffmpeg_mcm_to_mtl_audio( yes_overwrite=False, ) logger.debug(f"Tx command executed on {tx_host.name}: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # MTL Rx mtl_rx_inp = FFmpegMtlSt30pRx( # TODO: Verify the variables @@ -148,7 +150,9 @@ def test_st2110_ffmpeg_mcm_to_mtl_audio( yes_overwrite=True, ) logger.debug(f"Rx command executed on {rx_host.name}: {mtl_rx_ff.get_command()}") - mtl_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mtl_rx_ff) + mtl_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mtl_rx_ff + ) time.sleep(2) # wait for media_proxy to start mtl_rx_executor.start() diff --git a/tests/validation/functional/st2110/st30/test_ffmpeg_mtl_to_mcm_audio.py b/tests/validation/functional/st2110/st30/test_ffmpeg_mtl_to_mcm_audio.py index 6ddd69092..701f49ca8 100644 --- a/tests/validation/functional/st2110/st30/test_ffmpeg_mtl_to_mcm_audio.py +++ b/tests/validation/functional/st2110/st30/test_ffmpeg_mtl_to_mcm_audio.py @@ -36,7 +36,7 @@ @pytest.mark.parametrize("audio_type", [k for k in audio_files.keys()]) def test_st2110_ffmpeg_mtl_to_mcm_audio( - media_proxy, hosts, test_config, audio_type: str + media_proxy, hosts, test_config, audio_type: str, log_path ) -> None: # media_proxy fixture used only to ensure that the media proxy is running # Get TX and RX hosts @@ -115,7 +115,9 @@ def test_st2110_ffmpeg_mtl_to_mcm_audio( yes_overwrite=False, ) logger.debug(f"Tx command executed on {tx_host.name}: {mtl_tx_ff.get_command()}") - mtl_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mtl_tx_ff) + mtl_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mtl_tx_ff + ) # >>>>> MCM Rx mcm_rx_inp = FFmpegMcmST2110AudioRx( @@ -145,7 +147,9 @@ def test_st2110_ffmpeg_mtl_to_mcm_audio( yes_overwrite=True, ) logger.debug(f"Rx command executed on {rx_host.name}: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) time.sleep(2) # wait for media_proxy to start mcm_rx_executor.start() diff --git a/tests/validation/functional/test_demo.py b/tests/validation/functional/test_demo.py index 37f54bcff..6d8933e12 100644 --- a/tests/validation/functional/test_demo.py +++ b/tests/validation/functional/test_demo.py @@ -76,7 +76,9 @@ def test_list_command_on_sut(hosts): process.stop() -def test_mesh_agent_lifecycle(mesh_agent): +@pytest.mark.smoke +@pytest.mark.parametrize("logging", ["logging_on"]) +def test_mesh_agent_lifecycle(mesh_agent, logging): """Test starting and stopping the mesh agent.""" logger.info("Testing mesh_agent lifecycle") assert ( @@ -86,7 +88,9 @@ def test_mesh_agent_lifecycle(mesh_agent): logger.info("Mesh agent lifecycle test completed successfully.") -def test_media_proxy(media_proxy): +@pytest.mark.smoke +@pytest.mark.parametrize("logging", ["logging_on"]) +def test_media_proxy(media_proxy, logging): """Test starting and stopping the media proxy without sudo.""" logger.info("Testing media_proxy lifecycle") for proxy in media_proxy.values(): @@ -111,7 +115,9 @@ def test_sudo_command(hosts): logger.info("Sudo command execution test completed") -def test_demo_local_ffmpeg_video_integrity(media_proxy, hosts, test_config) -> None: +def test_demo_local_ffmpeg_video_integrity( + media_proxy, hosts, test_config, log_path +) -> None: # media_proxy fixture used only to ensure that the media proxy is running tx_host = rx_host = list(hosts.values())[0] prefix_variables = test_config.get("prefix_variables", {}) @@ -163,7 +169,9 @@ def test_demo_local_ffmpeg_video_integrity(media_proxy, hosts, test_config) -> N ) logger.debug(f"Tx command: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # >>>>> MCM Rx mcm_rx_inp = FFmpegMcmMemifVideoIO( @@ -192,7 +200,9 @@ def test_demo_local_ffmpeg_video_integrity(media_proxy, hosts, test_config) -> N ) logger.debug(f"Rx command: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) integrator = FileVideoIntegrityRunner( host=rx_host, @@ -215,7 +225,9 @@ def test_demo_local_ffmpeg_video_integrity(media_proxy, hosts, test_config) -> N assert result, "Integrity check failed" -def test_demo_local_ffmpeg_video_stream(media_proxy, hosts, test_config) -> None: +def test_demo_local_ffmpeg_video_stream( + media_proxy, hosts, test_config, log_path +) -> None: # media_proxy fixture used only to ensure that the media proxy is running tx_host = rx_host = list(hosts.values())[0] prefix_variables = test_config.get("prefix_variables", {}) @@ -268,7 +280,9 @@ def test_demo_local_ffmpeg_video_stream(media_proxy, hosts, test_config) -> None ) logger.debug(f"Tx command: {mcm_tx_ff.get_command()}") - mcm_tx_executor = FFmpegExecutor(tx_host, ffmpeg_instance=mcm_tx_ff) + mcm_tx_executor = FFmpegExecutor( + tx_host, log_path=log_path, ffmpeg_instance=mcm_tx_ff + ) # >>>>> MCM Rx mcm_rx_inp = FFmpegMcmMemifVideoIO( @@ -298,7 +312,9 @@ def test_demo_local_ffmpeg_video_stream(media_proxy, hosts, test_config) -> None ) logger.debug(f"Rx command: {mcm_rx_ff.get_command()}") - mcm_rx_executor = FFmpegExecutor(rx_host, ffmpeg_instance=mcm_rx_ff) + mcm_rx_executor = FFmpegExecutor( + rx_host, log_path=log_path, ffmpeg_instance=mcm_rx_ff + ) integrator = StreamVideoIntegrityRunner( host=rx_host, @@ -500,3 +516,10 @@ def test_build_mtl_ffmpeg(build_mtl_ffmpeg, hosts, test_config): """ logger.info("Testing MTL FFmpeg build process") assert build_mtl_ffmpeg, "MTL FFmpeg build failed" + + +def test_simple(log_path_dir, log_path, request): + # For this test, log_path will be based on "test_simple" + logging.info(f"Log path dir for test_simple: {log_path_dir}") + logging.info(f"Log path for test_simple: {log_path}") + logging.info(f"Request: {request.node.name}") diff --git a/tests/validation/pytest.ini b/tests/validation/pytest.ini index 7d6a37fa3..e61cd753c 100644 --- a/tests/validation/pytest.ini +++ b/tests/validation/pytest.ini @@ -1,8 +1,7 @@ [pytest] pythonpath = . -log_cli = true log_cli_level = info log_file = pytest.log log_file_level = debug -log_file_format = %(asctime)s,%(msecs)03d %(levelname)-8s %(filename)s:%(lineno)d %(message)s -log_file_date_format = %Y-%m-%d %H:%M:%S \ No newline at end of file +markers = + smoke: marks tests as smoke tests (deselect with '-m "not smoke"') \ No newline at end of file