diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..f577ae8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: "CodeQL" + +on: + push: + tags: + - "*" + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: + - actions + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index d1c2d64..33c07df 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -10,7 +10,7 @@ on: branches: - main schedule: - - cron: '0 0 * * *' + - cron: "0 6 * * 1" workflow_dispatch: permissions: @@ -24,6 +24,8 @@ env: jobs: build: runs-on: ubuntu-latest + env: + HAS_DOCKERHUB_SECRETS: ${{ github.event_name != 'pull_request' || github.repository == github.event.pull_request.head.repo.full_name }} steps: - name: Checkout uses: actions/checkout@v4 @@ -32,6 +34,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub + if: ${{ env.HAS_DOCKERHUB_SECRETS }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -64,6 +67,7 @@ jobs: with: context: . platforms: ${{ env.PLATFORMS }} + pull: true cache-from: type=gha cache-to: type=gha push: true @@ -74,17 +78,15 @@ jobs: if: ${{ github.event_name == 'pull_request' }} uses: docker/scout-action@v1 with: - command: cves,recommendations,compare + command: cves,compare image: ${{ steps.meta.outputs.tags }} to: ${{ vars.GHCR_IMAGE }}:latest - ignore-base: true ignore-unchanged: true only-fixed: true - # only-severities: critical,high write-comment: true github-token: ${{ secrets.GITHUB_TOKEN }} - name: Update repo description - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' && env.HAS_DOCKERHUB_SECRETS }} uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9368284 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,579 @@ +name: Tests + +on: + push: + tags: + - "*" + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +env: + RESULTS_FILE: /tmp/test_results.txt + +jobs: + input-validation: + name: Input Validation Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test helpers + run: | + cat > /tmp/test_helpers.sh << 'EOF' + GREEN='\033[32m' + RED='\033[31m' + RESET='\033[0m' + + # Run a test that should succeed + run_valid() { + local name="$1" + shift + if "$@" > /dev/null 2>&1; then + echo -e "${GREEN}✓ Passed${RESET}: $name" + echo "pass:$name" >> "$RESULTS_FILE" + else + echo -e "${RED}✗ Failed${RESET}: $name" + echo "fail:$name" >> "$RESULTS_FILE" + fi + return 0 # Always return success to continue running tests + } + + # Run a test that should fail (invalid input) + run_invalid() { + local name="$1" + shift + if "$@" > /dev/null 2>&1; then + echo -e "${RED}✗ Failed (should have been rejected)${RESET}: $name" + echo "fail:$name" >> "$RESULTS_FILE" + else + echo -e "${GREEN}✓ Passed (correctly rejected)${RESET}: $name" + echo "pass:$name" >> "$RESULTS_FILE" + fi + return 0 # Always return success to continue running tests + } + EOF + touch "$RESULTS_FILE" + + - name: Build test image + run: docker build -t diskmark:test . + + # ============================================ + # VALID INPUT TESTS - Should all succeed + # ============================================ + + - name: "Valid: Default configuration" + run: | + source /tmp/test_helpers.sh + run_valid "Default configuration" docker run --rm -e DRY_RUN=1 diskmark:test + + - name: "Valid: SIZE variations" + run: | + source /tmp/test_helpers.sh + run_valid "SIZE=512K" docker run --rm -e DRY_RUN=1 -e SIZE=512K diskmark:test + run_valid "SIZE=100M" docker run --rm -e DRY_RUN=1 -e SIZE=100M diskmark:test + run_valid "SIZE=1G" docker run --rm -e DRY_RUN=1 -e SIZE=1G diskmark:test + run_valid "SIZE=2T" docker run --rm -e DRY_RUN=1 -e SIZE=2T diskmark:test + run_valid "SIZE=1P" docker run --rm -e DRY_RUN=1 -e SIZE=1P diskmark:test + run_valid "SIZE=100m (lowercase)" docker run --rm -e DRY_RUN=1 -e SIZE=100m diskmark:test + run_valid "SIZE=1g (lowercase)" docker run --rm -e DRY_RUN=1 -e SIZE=1g diskmark:test + + - name: "Valid: PROFILE options" + run: | + source /tmp/test_helpers.sh + run_valid "PROFILE=auto" docker run --rm -e DRY_RUN=1 -e PROFILE=auto diskmark:test + run_valid "PROFILE=default" docker run --rm -e DRY_RUN=1 -e PROFILE=default diskmark:test + run_valid "PROFILE=nvme" docker run --rm -e DRY_RUN=1 -e PROFILE=nvme diskmark:test + + - name: "Valid: IO modes" + run: | + source /tmp/test_helpers.sh + run_valid "IO=direct" docker run --rm -e DRY_RUN=1 -e IO=direct diskmark:test + run_valid "IO=buffered" docker run --rm -e DRY_RUN=1 -e IO=buffered diskmark:test + + - name: "Valid: DATA patterns" + run: | + source /tmp/test_helpers.sh + run_valid "DATA=random" docker run --rm -e DRY_RUN=1 -e DATA=random diskmark:test + run_valid "DATA=rand" docker run --rm -e DRY_RUN=1 -e DATA=rand diskmark:test + run_valid "DATA=zero" docker run --rm -e DRY_RUN=1 -e DATA=zero diskmark:test + run_valid "DATA=0" docker run --rm -e DRY_RUN=1 -e DATA=0 diskmark:test + run_valid "DATA=0x00" docker run --rm -e DRY_RUN=1 -e DATA=0x00 diskmark:test + + - name: "Valid: WARMUP options" + run: | + source /tmp/test_helpers.sh + run_valid "WARMUP=0" docker run --rm -e DRY_RUN=1 -e WARMUP=0 diskmark:test + run_valid "WARMUP=1" docker run --rm -e DRY_RUN=1 -e WARMUP=1 diskmark:test + run_valid "WARMUP=1 WARMUP_SIZE=8M" docker run --rm -e DRY_RUN=1 -e WARMUP=1 -e WARMUP_SIZE=8M diskmark:test + run_valid "WARMUP=1 WARMUP_SIZE=64M" docker run --rm -e DRY_RUN=1 -e WARMUP=1 -e WARMUP_SIZE=64M diskmark:test + run_valid "WARMUP=1 WARMUP_SIZE=128M" docker run --rm -e DRY_RUN=1 -e WARMUP=1 -e WARMUP_SIZE=128M diskmark:test + + - name: "Valid: RUNTIME formats" + run: | + source /tmp/test_helpers.sh + run_valid "RUNTIME=500ms" docker run --rm -e DRY_RUN=1 -e RUNTIME=500ms diskmark:test + run_valid "RUNTIME=5s" docker run --rm -e DRY_RUN=1 -e RUNTIME=5s diskmark:test + run_valid "RUNTIME=2m" docker run --rm -e DRY_RUN=1 -e RUNTIME=2m diskmark:test + run_valid "RUNTIME=1h" docker run --rm -e DRY_RUN=1 -e RUNTIME=1h diskmark:test + + - name: "Valid: LOOPS option" + run: | + source /tmp/test_helpers.sh + run_valid "LOOPS=1" docker run --rm -e DRY_RUN=1 -e LOOPS=1 diskmark:test + run_valid "LOOPS=5" docker run --rm -e DRY_RUN=1 -e LOOPS=5 diskmark:test + run_valid "LOOPS=100" docker run --rm -e DRY_RUN=1 -e LOOPS=100 diskmark:test + + - name: "Valid: Custom JOB formats" + run: | + source /tmp/test_helpers.sh + run_valid "JOB=SEQ1MQ8T1" docker run --rm -e DRY_RUN=1 -e JOB=SEQ1MQ8T1 diskmark:test + run_valid "JOB=SEQ128KQ32T1" docker run --rm -e DRY_RUN=1 -e JOB=SEQ128KQ32T1 diskmark:test + run_valid "JOB=RND4KQ32T16" docker run --rm -e DRY_RUN=1 -e JOB=RND4KQ32T16 diskmark:test + run_valid "JOB=RND4KQ1T1" docker run --rm -e DRY_RUN=1 -e JOB=RND4KQ1T1 diskmark:test + run_valid "JOB=SEQ4MQ1T4" docker run --rm -e DRY_RUN=1 -e JOB=SEQ4MQ1T4 diskmark:test + + - name: "Valid: Combined parameters" + run: | + source /tmp/test_helpers.sh + run_valid "SIZE+WARMUP+PROFILE+IO+DATA" docker run --rm -e DRY_RUN=1 -e SIZE=2G -e WARMUP=1 -e WARMUP_SIZE=64M -e PROFILE=nvme -e IO=direct -e DATA=random diskmark:test + run_valid "SIZE+LOOPS+DATA+IO" docker run --rm -e DRY_RUN=1 -e SIZE=512M -e LOOPS=3 -e DATA=zero -e IO=buffered diskmark:test + + # ============================================ + # INVALID INPUT TESTS - Should all fail + # ============================================ + + - name: "Invalid: SIZE - negative value" + run: | + source /tmp/test_helpers.sh + run_invalid "SIZE=-1G" docker run --rm -e DRY_RUN=1 -e SIZE=-1G diskmark:test + + - name: "Invalid: SIZE - zero value" + run: | + source /tmp/test_helpers.sh + run_invalid "SIZE=0" docker run --rm -e DRY_RUN=1 -e SIZE=0 diskmark:test + + - name: "Invalid: SIZE - invalid unit" + run: | + source /tmp/test_helpers.sh + run_invalid "SIZE=1X" docker run --rm -e DRY_RUN=1 -e SIZE=1X diskmark:test + + - name: "Invalid: SIZE - non-numeric" + run: | + source /tmp/test_helpers.sh + run_invalid "SIZE=abc" docker run --rm -e DRY_RUN=1 -e SIZE=abc diskmark:test + + - name: "Valid: SIZE - empty string (defaults to 1G)" + run: | + source /tmp/test_helpers.sh + run_valid "SIZE=(empty, defaults to 1G)" docker run --rm -e DRY_RUN=1 -e SIZE= diskmark:test + + - name: "Invalid: SIZE - float value" + run: | + source /tmp/test_helpers.sh + run_invalid "SIZE=2.5G" docker run --rm -e DRY_RUN=1 -e SIZE=2.5G diskmark:test + + - name: "Invalid: WARMUP - not 0 or 1" + run: | + source /tmp/test_helpers.sh + run_invalid "WARMUP=2" docker run --rm -e DRY_RUN=1 -e WARMUP=2 diskmark:test + run_invalid "WARMUP=yes" docker run --rm -e DRY_RUN=1 -e WARMUP=yes diskmark:test + run_invalid "WARMUP=-1" docker run --rm -e DRY_RUN=1 -e WARMUP=-1 diskmark:test + + - name: "Invalid: WARMUP_SIZE - invalid format" + run: | + source /tmp/test_helpers.sh + run_invalid "WARMUP_SIZE=invalid" docker run --rm -e DRY_RUN=1 -e WARMUP=1 -e WARMUP_SIZE=invalid diskmark:test + run_invalid "WARMUP_SIZE=-8M" docker run --rm -e DRY_RUN=1 -e WARMUP=1 -e WARMUP_SIZE=-8M diskmark:test + + - name: "Invalid: IO - invalid mode" + run: | + source /tmp/test_helpers.sh + run_invalid "IO=sync" docker run --rm -e DRY_RUN=1 -e IO=sync diskmark:test + run_invalid "IO=async" docker run --rm -e DRY_RUN=1 -e IO=async diskmark:test + + - name: "Invalid: DATA - invalid pattern" + run: | + source /tmp/test_helpers.sh + run_invalid "DATA=ones" docker run --rm -e DRY_RUN=1 -e DATA=ones diskmark:test + run_invalid "DATA=0xFF" docker run --rm -e DRY_RUN=1 -e DATA=0xFF diskmark:test + + - name: "Invalid: PROFILE - invalid value" + run: | + source /tmp/test_helpers.sh + run_invalid "PROFILE=fast" docker run --rm -e DRY_RUN=1 -e PROFILE=fast diskmark:test + run_invalid "PROFILE=ssd" docker run --rm -e DRY_RUN=1 -e PROFILE=ssd diskmark:test + + - name: "Invalid: RUNTIME - invalid format" + run: | + source /tmp/test_helpers.sh + run_invalid "RUNTIME=5 (no unit)" docker run --rm -e DRY_RUN=1 -e RUNTIME=5 diskmark:test + run_invalid "RUNTIME=5sec" docker run --rm -e DRY_RUN=1 -e RUNTIME=5sec diskmark:test + run_invalid "RUNTIME=-5s" docker run --rm -e DRY_RUN=1 -e RUNTIME=-5s diskmark:test + + - name: "Invalid: LOOPS - not a positive integer" + run: | + source /tmp/test_helpers.sh + run_invalid "LOOPS=0" docker run --rm -e DRY_RUN=1 -e LOOPS=0 diskmark:test + run_invalid "LOOPS=-1" docker run --rm -e DRY_RUN=1 -e LOOPS=-1 diskmark:test + run_invalid "LOOPS=abc" docker run --rm -e DRY_RUN=1 -e LOOPS=abc diskmark:test + + - name: "Valid: LOOPS and RUNTIME together" + run: | + source /tmp/test_helpers.sh + run_valid "LOOPS+RUNTIME (hybrid mode)" docker run --rm -e DRY_RUN=1 -e LOOPS=5 -e RUNTIME=10s diskmark:test + + - name: "Invalid: JOB - malformed job name" + run: | + source /tmp/test_helpers.sh + run_invalid "JOB=INVALID" docker run --rm -e DRY_RUN=1 -e JOB=INVALID diskmark:test + run_invalid "JOB=SEQ1M (missing Q and T)" docker run --rm -e DRY_RUN=1 -e JOB=SEQ1M diskmark:test + run_invalid "JOB=RND4KQ32 (missing T)" docker run --rm -e DRY_RUN=1 -e JOB=RND4KQ32 diskmark:test + run_invalid "JOB=XXX4KQ32T1 (invalid prefix)" docker run --rm -e DRY_RUN=1 -e JOB=XXX4KQ32T1 diskmark:test + + - name: "Invalid: DRY_RUN - not 0 or 1" + run: | + source /tmp/test_helpers.sh + run_invalid "DRY_RUN=yes" docker run --rm -e DRY_RUN=yes diskmark:test + + # ============================================ + # EDGE CASE TESTS + # ============================================ + + - name: "Edge: Very large SIZE value" + run: | + source /tmp/test_helpers.sh + run_valid "SIZE=999P" docker run --rm -e DRY_RUN=1 -e SIZE=999P diskmark:test + + - name: "Edge: Minimum valid SIZE" + run: | + source /tmp/test_helpers.sh + run_valid "SIZE=1 (bytes)" docker run --rm -e DRY_RUN=1 -e SIZE=1 diskmark:test + + - name: "Edge: Very short RUNTIME" + run: | + source /tmp/test_helpers.sh + run_valid "RUNTIME=1ms" docker run --rm -e DRY_RUN=1 -e RUNTIME=1ms diskmark:test + + - name: "Edge: Very long RUNTIME" + run: | + source /tmp/test_helpers.sh + run_valid "RUNTIME=999h" docker run --rm -e DRY_RUN=1 -e RUNTIME=999h diskmark:test + + - name: "Edge: High LOOPS value" + run: | + source /tmp/test_helpers.sh + run_valid "LOOPS=9999" docker run --rm -e DRY_RUN=1 -e LOOPS=9999 diskmark:test + + # ============================================ + # VALIDATION SUMMARY + # ============================================ + + - name: Test Summary + if: always() + run: | + echo "" + echo "============================================" + echo " INPUT VALIDATION TEST SUMMARY" + echo "============================================" + echo "" + + PASS_COUNT=$(grep -c "^pass:" "$RESULTS_FILE" 2>/dev/null || echo 0) + FAIL_COUNT=$(grep -c "^fail:" "$RESULTS_FILE" 2>/dev/null || echo 0) + TOTAL=$((PASS_COUNT + FAIL_COUNT)) + + echo "✅ Passed: $PASS_COUNT" + echo "❌ Failed: $FAIL_COUNT" + echo "━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Total: $TOTAL" + echo "" + + if [ "$FAIL_COUNT" -gt 0 ]; then + echo "Failed tests:" + grep "^fail:" "$RESULTS_FILE" | cut -d: -f2- | while read -r test; do + echo " ❌ $test" + done + echo "" + exit 1 + else + echo "All tests passed! 🎉" + fi + + # ============================================ + # REAL BENCHMARK TESTS (non dry-run) + # ============================================ + benchmark: + name: Benchmark Tests + runs-on: ubuntu-latest + needs: input-validation + env: + RESULTS_FILE: /tmp/benchmark_results.txt + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup test helpers + run: | + cat > /tmp/test_helpers.sh << 'EOF' + GREEN='\033[32m' + RED='\033[31m' + RESET='\033[0m' + + run_benchmark() { + local name="$1" + shift + if "$@"; then + echo -e "${GREEN}✓ Passed${RESET}: $name" + echo "pass:$name" >> "$RESULTS_FILE" + else + echo -e "${RED}✗ Failed${RESET}: $name" + echo "fail:$name" >> "$RESULTS_FILE" + fi + return 0 # Always return success to continue running tests + } + EOF + touch "$RESULTS_FILE" + + - name: Build test image + run: docker build -t diskmark:test . + + # ---------- PROFILE variations ---------- + - name: "PROFILE: auto" + run: | + source /tmp/test_helpers.sh + run_benchmark "PROFILE=auto" docker run --rm -e SIZE=16M -e RUNTIME=1s -e PROFILE=auto diskmark:test + + - name: "PROFILE: default" + run: | + source /tmp/test_helpers.sh + run_benchmark "PROFILE=default" docker run --rm -e SIZE=16M -e RUNTIME=1s -e PROFILE=default diskmark:test + + - name: "PROFILE: nvme" + run: | + source /tmp/test_helpers.sh + run_benchmark "PROFILE=nvme" docker run --rm -e SIZE=16M -e RUNTIME=1s -e PROFILE=nvme diskmark:test + + # ---------- IO variations ---------- + - name: "IO: direct" + run: | + source /tmp/test_helpers.sh + run_benchmark "IO=direct" docker run --rm -e SIZE=16M -e RUNTIME=1s -e IO=direct diskmark:test + + - name: "IO: buffered" + run: | + source /tmp/test_helpers.sh + run_benchmark "IO=buffered" docker run --rm -e SIZE=16M -e RUNTIME=1s -e IO=buffered diskmark:test + + # ---------- DATA variations ---------- + - name: "DATA: random" + run: | + source /tmp/test_helpers.sh + run_benchmark "DATA=random" docker run --rm -e SIZE=16M -e RUNTIME=1s -e DATA=random diskmark:test + + - name: "DATA: rand" + run: | + source /tmp/test_helpers.sh + run_benchmark "DATA=rand" docker run --rm -e SIZE=16M -e RUNTIME=1s -e DATA=rand diskmark:test + + - name: "DATA: zero" + run: | + source /tmp/test_helpers.sh + run_benchmark "DATA=zero" docker run --rm -e SIZE=16M -e RUNTIME=1s -e DATA=zero diskmark:test + + - name: "DATA: 0" + run: | + source /tmp/test_helpers.sh + run_benchmark "DATA=0" docker run --rm -e SIZE=16M -e RUNTIME=1s -e DATA=0 diskmark:test + + - name: "DATA: 0x00" + run: | + source /tmp/test_helpers.sh + run_benchmark "DATA=0x00" docker run --rm -e SIZE=16M -e RUNTIME=1s -e DATA=0x00 diskmark:test + + # ---------- SIZE variations ---------- + - name: "SIZE: bytes only" + run: | + source /tmp/test_helpers.sh + run_benchmark "SIZE=1048576 (bytes)" docker run --rm -e SIZE=1048576 -e RUNTIME=1s diskmark:test + + - name: "SIZE: kilobytes" + run: | + source /tmp/test_helpers.sh + run_benchmark "SIZE=512K" docker run --rm -e SIZE=512K -e RUNTIME=1s diskmark:test + + - name: "SIZE: megabytes" + run: | + source /tmp/test_helpers.sh + run_benchmark "SIZE=16M" docker run --rm -e SIZE=16M -e RUNTIME=1s diskmark:test + + - name: "SIZE: lowercase unit" + run: | + source /tmp/test_helpers.sh + run_benchmark "SIZE=16m (lowercase)" docker run --rm -e SIZE=16m -e RUNTIME=1s diskmark:test + + # ---------- WARMUP variations ---------- + - name: "WARMUP: disabled" + run: | + source /tmp/test_helpers.sh + run_benchmark "WARMUP=0" docker run --rm -e SIZE=16M -e RUNTIME=1s -e WARMUP=0 diskmark:test + + - name: "WARMUP: enabled (default block)" + run: | + source /tmp/test_helpers.sh + run_benchmark "WARMUP=1" docker run --rm -e SIZE=16M -e RUNTIME=1s -e WARMUP=1 diskmark:test + + - name: "WARMUP: with custom WARMUP_SIZE" + run: | + source /tmp/test_helpers.sh + run_benchmark "WARMUP=1 WARMUP_SIZE=4M" docker run --rm -e SIZE=16M -e RUNTIME=1s -e WARMUP=1 -e WARMUP_SIZE=4M diskmark:test + + # ---------- RUNTIME variations ---------- + - name: "RUNTIME: milliseconds" + run: | + source /tmp/test_helpers.sh + run_benchmark "RUNTIME=500ms" docker run --rm -e SIZE=16M -e RUNTIME=500ms diskmark:test + + - name: "RUNTIME: seconds" + run: | + source /tmp/test_helpers.sh + run_benchmark "RUNTIME=1s" docker run --rm -e SIZE=16M -e RUNTIME=1s diskmark:test + + # ---------- LOOPS variations ---------- + - name: "LOOPS: single loop" + run: | + source /tmp/test_helpers.sh + run_benchmark "LOOPS=1" docker run --rm -e SIZE=16M -e LOOPS=1 diskmark:test + + - name: "LOOPS: multiple loops" + run: | + source /tmp/test_helpers.sh + run_benchmark "LOOPS=2" docker run --rm -e SIZE=16M -e LOOPS=2 diskmark:test + + # ---------- JOB variations ---------- + - name: "JOB: sequential kilobytes" + run: | + source /tmp/test_helpers.sh + run_benchmark "JOB=SEQ128KQ1T1" docker run --rm -e SIZE=16M -e RUNTIME=1s -e JOB=SEQ128KQ1T1 diskmark:test + + - name: "JOB: sequential megabytes" + run: | + source /tmp/test_helpers.sh + run_benchmark "JOB=SEQ1MQ8T1" docker run --rm -e SIZE=16M -e RUNTIME=1s -e JOB=SEQ1MQ8T1 diskmark:test + + - name: "JOB: random access" + run: | + source /tmp/test_helpers.sh + run_benchmark "JOB=RND4KQ1T1" docker run --rm -e SIZE=16M -e RUNTIME=1s -e JOB=RND4KQ1T1 diskmark:test + + - name: "JOB: high queue depth" + run: | + source /tmp/test_helpers.sh + run_benchmark "JOB=RND4KQ32T1" docker run --rm -e SIZE=16M -e RUNTIME=1s -e JOB=RND4KQ32T1 diskmark:test + + - name: "JOB: multiple threads" + run: | + source /tmp/test_helpers.sh + run_benchmark "JOB=RND4KQ1T4" docker run --rm -e SIZE=16M -e RUNTIME=1s -e JOB=RND4KQ1T4 diskmark:test + + # ---------- COMBINED PARAMETERS ---------- + - name: "Combo: NVMe profile + warmup + random data" + run: | + source /tmp/test_helpers.sh + run_benchmark "NVMe+warmup+random" docker run --rm -e SIZE=16M -e RUNTIME=1s -e PROFILE=nvme -e WARMUP=1 -e DATA=random diskmark:test + + - name: "Combo: Default profile + zero data + loops" + run: | + source /tmp/test_helpers.sh + run_benchmark "default+zero+loops" docker run --rm -e SIZE=16M -e LOOPS=1 -e PROFILE=default -e DATA=zero diskmark:test + + - name: "Combo: Buffered IO + warmup + custom block size" + run: | + source /tmp/test_helpers.sh + run_benchmark "buffered+warmup+custom" docker run --rm -e SIZE=16M -e RUNTIME=1s -e IO=buffered -e WARMUP=1 -e WARMUP_SIZE=2M diskmark:test + + - name: "Combo: Custom job + warmup + zero data" + run: | + source /tmp/test_helpers.sh + run_benchmark "job+warmup+zero" docker run --rm -e SIZE=16M -e RUNTIME=1s -e JOB=SEQ1MQ1T1 -e WARMUP=1 -e DATA=zero diskmark:test + + # ---------- EDGE CASES ---------- + - name: "Edge: Cross-profile (NVMe profile with buffered IO)" + run: | + source /tmp/test_helpers.sh + run_benchmark "NVMe+buffered" docker run --rm -e SIZE=16M -e RUNTIME=1s -e PROFILE=nvme -e IO=buffered diskmark:test + + - name: "Edge: Mixed data and IO modes" + run: | + source /tmp/test_helpers.sh + run_benchmark "0x00+buffered+warmup" docker run --rm -e SIZE=16M -e RUNTIME=1s -e WARMUP=1 -e DATA=0x00 -e IO=buffered diskmark:test + + - name: "Edge: Custom job with PROFILE set (job takes precedence)" + run: | + source /tmp/test_helpers.sh + run_benchmark "job+profile" docker run --rm -e SIZE=16M -e RUNTIME=1s -e JOB=RND4KQ32T1 -e PROFILE=nvme diskmark:test + + - name: "Edge: Small warmup block size" + run: | + source /tmp/test_helpers.sh + run_benchmark "WARMUP_SIZE=1K" docker run --rm -e SIZE=16M -e RUNTIME=1s -e WARMUP=1 -e WARMUP_SIZE=1K diskmark:test + + - name: "Edge: WARMUP_SIZE set with WARMUP=0 (ignored)" + run: | + source /tmp/test_helpers.sh + run_benchmark "WARMUP=0+WARMUP_SIZE" docker run --rm -e SIZE=16M -e RUNTIME=1s -e WARMUP=0 -e WARMUP_SIZE=64M diskmark:test + + - name: "Edge: All parameters explicitly set" + run: | + source /tmp/test_helpers.sh + run_benchmark "all params" docker run --rm -e SIZE=16M -e RUNTIME=1s -e PROFILE=default -e IO=direct -e DATA=random -e WARMUP=1 -e WARMUP_SIZE=4M diskmark:test + + - name: "Edge: Minimal size and runtime" + run: | + source /tmp/test_helpers.sh + run_benchmark "SIZE=4K RUNTIME=100ms" docker run --rm -e SIZE=4K -e RUNTIME=100ms diskmark:test + + - name: "Edge: SIZE smaller than WARMUP_SIZE" + run: | + source /tmp/test_helpers.sh + run_benchmark "SIZE/dev/null || echo 0) + FAIL_COUNT=$(grep -c "^fail:" "$RESULTS_FILE" 2>/dev/null || echo 0) + TOTAL=$((PASS_COUNT + FAIL_COUNT)) + + echo "✅ Passed: $PASS_COUNT" + echo "❌ Failed: $FAIL_COUNT" + echo "━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Total: $TOTAL" + echo "" + + if [ "$FAIL_COUNT" -gt 0 ]; then + echo "Failed tests:" + grep "^fail:" "$RESULTS_FILE" | cut -d: -f2- | while read -r test; do + echo " ❌ $test" + done + echo "" + exit 1 + else + echo "All benchmarks passed! 🎉" + fi diff --git a/Dockerfile b/Dockerfile index cf53612..da31ebb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,27 @@ -FROM ubuntu:rolling AS deps -RUN export DEBIAN_FRONTEND=noninteractive \ - && apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y fio \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* +FROM alpine:latest AS builder +RUN apk add --no-cache bash coreutils fio findmnt grep ncurses ncurses-terminfo-base perl procps sed util-linux +RUN mkdir -p /dist/usr/bin /dist/lib /dist/usr/lib /dist/etc /dist/disk && \ + for bin in bash cat cut dd df grep ls mkdir rm sed awk basename dirname env expr fio findmnt free head lsblk numfmt perl printf tail tput tr wc; do cp $(which $bin) /dist/usr/bin/; done && \ + cp /lib/ld-musl-*.so.1 /dist/lib/ && cp -a /lib/*.so* /dist/usr/lib/ 2>/dev/null || true && \ + cp -a /usr/lib/*.so* /dist/usr/lib/ 2>/dev/null || true && \ + cp /usr/lib/perl5/core_perl/CORE/libperl.so /dist/usr/lib/ && \ + cp -r /etc/terminfo /dist/etc/ && \ + echo "nobody:x:65534:65534:Nobody:/:" > /dist/etc/passwd && \ + chown 65534:65534 /dist/disk -FROM deps +FROM scratch +COPY --from=builder /dist/ / COPY diskmark.sh /usr/bin/diskmark VOLUME /disk WORKDIR /disk -ENV TARGET="/disk" -ENV PROFILE="auto" -ENV IO="direct" -ENV DATA="random" -ENV SIZE="1G" -ENV WARMUP="1" -ENV RUNTIME="5s" -ENTRYPOINT [ "diskmark" ] +USER 65534:65534 +ENV TERM="xterm" \ + TARGET="/disk" \ + PROFILE="auto" \ + IO="direct" \ + DATA="random" \ + SIZE="1G" \ + WARMUP="1" \ + WARMUP_SIZE="" \ + RUNTIME="5s" +ENTRYPOINT ["/usr/bin/bash", "/usr/bin/diskmark"] diff --git a/README.md b/README.md index 0d9743b..d84adbe 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,19 @@ The container contains two different test profiles: ## Advanced usage Find below a table listing all the different parameters you can use with the container: -| Parameter | Type | Default | Description | -| :- | :- |:- | :- | -| `PROFILE` | Environment | auto | The profile to apply:
- `auto` to try and autoselect the best one based on the used drive detection,
- `default`, best suited for hard disk drives,
- `nvme`, best suited for NMVe SSD drives. | -| `JOB` | Environment | | A custom job to use: details below in the [Custom job](#custom-job) section.
This parameter overrides the `PROFILE` parameter. | -| `IO` | Environment | direct | The drive access mode:
- `direct` for synchronous I/O,
- `buffered` for asynchronous I/O. | -| `DATA` | Environment | random | The test data:
- `random` to use random data,
- `0x00` to fill with 0 (zero) values. | -| `SIZE` | Environment | 1G | The size of the test file in bytes. | -| `WARMUP` | Environment | 0 | When set to `1`, use a warmup phase, thus preparing the test file with `fallocate`, using either random data or zero values as set by `DATA`. | -| `RUNTIME` | Environment | 5s | The test duration for each job. | -| `LOOPS` | Environment | | The number of test loops performed on the test file.
This parameter overrides the `RUNTIME` parameter. | -| `/disk` | Volume | | The target path to benchmark. | +| Parameter | Type | Default | Description | +| :- | :- |:- | :- | +| `PROFILE` | Environment | `auto` | The profile to apply:
- `auto` to try and autoselect the best one based on the used drive detection,
- `default`, best suited for hard disk drives,
- `nvme`, best suited for NVMe SSD drives. | +| `JOB` | Environment | | A custom job to use: details below in the [Custom job](#custom-job) section.
This parameter overrides the `PROFILE` parameter. | +| `IO` | Environment | `direct` | The drive access mode:
- `direct` for synchronous I/O,
- `buffered` for asynchronous I/O. | +| `DATA` | Environment | `random` | The test data:
- `random` to use random data,
- `0x00` to fill with 0 (zero) values. | +| `SIZE` | Environment | `1G` | The size of the test file (e.g., `500M`, `1G`, `10G`). | +| `WARMUP` | Environment | `0` | When set to `1`, use a warmup phase, thus preparing the test file with `dd`, using either random data or zero values as set by `DATA`. | +| `WARMUP_SIZE` | Environment | | Warmup block size. Defaults depend on the profile:
- `8M` for the default profile
- `64M` for the NVMe profile. | +| `RUNTIME` | Environment | `5s` | The duration for each job (e.g., `1s`, `5s`, `2m`).
Used alone: time-based benchmark.
Used with `LOOPS`: caps each loop to this duration. | +| `LOOPS` | Environment | | The number of test loops to run.
Used alone: runs exactly N loops with no time limit.
Used with `RUNTIME`: runs N loops, each capped to `RUNTIME`. | +| `DRY_RUN` | Environment | `0` | When set to `1`, validates configuration without running the benchmark. | +| `/disk` | Volume | | The target path to benchmark. | By default, a 1 GB test file is used, with a 5 seconds duration for each test, reading and writing random bytes on the disk where Docker is installed. @@ -50,6 +52,16 @@ You can achieve this using the following command: docker run -it --rm -e SIZE=4G -e WARMUP=1 -e LOOPS=2 -e DATA=0x00 e7db/diskmark ``` +You can also combine `LOOPS` and `RUNTIME` for hybrid mode — run a fixed number of loops, but cap each loop's duration: +``` +docker run -it --rm -e SIZE=1G -e LOOPS=3 -e RUNTIME=10s e7db/diskmark +``` + +Warmup block size is tunable with `WARMUP_SIZE` (e.g. `8M`, `64M`, `128M`). By default it adapts to the selected profile: `8M` for the default profile (HDD-friendly) and `64M` for the NVMe profile. You can override it explicitly if needed: +``` +docker run -it --rm -e WARMUP=1 -e WARMUP_SIZE=128M e7db/diskmark +``` + ### Force profile A detection of your disk is tried, so the benchmark uses the appropriate profile, `default` or `nvme`. diff --git a/diskmark.sh b/diskmark.sh index 3f63448..b1ada69 100755 --- a/diskmark.sh +++ b/diskmark.sh @@ -2,6 +2,35 @@ set -e +detect_color_support() { + if [[ "$TERM" == "dumb" ]]; then + echo 0 + else + echo 1 + fi +} + +detect_emoji_support() { + if [[ "$TERM" == "dumb" ]]; then + echo 0 + else + echo 1 + fi +} + +if [[ -z "$COLOR" ]]; then + COLOR=$(detect_color_support) +elif [[ ! "$COLOR" =~ ^[01]$ ]]; then + echo "Error: COLOR must be either 0 or 1." >&2 + exit 1 +fi +if [[ -z "$EMOJI" ]]; then + EMOJI=$(detect_emoji_support) +elif [[ ! "$EMOJI" =~ ^[01]$ ]]; then + echo "Error: EMOJI must be either 0 or 1." >&2 + exit 1 +fi + RESET="0m" NORMAL="0" BOLD="1" @@ -15,9 +44,25 @@ CYAN=";36m" WHITE=";37m" function color() { - echo "\e[$1$2" + if [[ "$COLOR" -eq 1 ]]; then + echo "\e[$1$2" + else + echo "" + fi } +if [[ "$EMOJI" -eq 1 ]]; then + SYM_SUCCESS="✅" + SYM_FAILURE="❌" + SYM_STOP="🛑" + SYM_ARROW="➤" +else + SYM_SUCCESS="[OK]" + SYM_FAILURE="[FAIL]" + SYM_STOP="[STOP]" + SYM_ARROW=">" +fi + function clean() { [[ -z $TARGET ]] && return if [[ -n $ISNEWDIR ]]; then @@ -29,9 +74,9 @@ function clean() { function interrupt() { local EXIT_CODE="${1:-0}" - echo -e "\r\n\n🛑 The benchmark was $(color $BOLD $RED)interrupted$(color $RESET)." + echo -e "\r\n\n$SYM_STOP The benchmark was $(color $BOLD $RED)interrupted$(color $RESET)." if [ ! -z "$2" ]; then - echo -e "➤ $2" + echo -e "$SYM_ARROW $2" fi clean exit "${EXIT_CODE}" @@ -40,9 +85,9 @@ trap 'interrupt $? "The benchmark was aborted before its completion."' HUP INT Q function fail() { local EXIT_CODE="${1:-1}" - echo -e "\r\n\n❌ The benchmark had $(color $BOLD $RED)failed$(color $RESET)." + echo -e "\r\n\n$SYM_FAILURE The benchmark had $(color $BOLD $RED)failed$(color $RESET)." if [ ! -z "$2" ]; then - echo -e "➤ $2" + echo -e "$SYM_ARROW $2" fi clean exit "${EXIT_CODE}" @@ -51,19 +96,87 @@ trap 'fail $? "The benchmark failed before its completion."' ERR function error() { local EXIT_CODE="${1:-1}" - echo -e "\r\n❌ The benchmark encountered an $(color $BOLD $RED)error$(color $RESET)." + echo -e "\r\n$SYM_FAILURE The benchmark encountered an $(color $BOLD $RED)error$(color $RESET)." if [ ! -z "$2" ]; then - echo -e "➤ $2" + echo -e "$SYM_ARROW $2" fi clean exit "${EXIT_CODE}" } +function requireCommand() { + command -v "$1" >/dev/null 2>&1 || fail 1 "Missing required dependency: $(color $BOLD $WHITE)$1$(color $RESET). Please install it and try again." +} + +function validateSizeString() { + local VALUE="$1" + local LABEL="$2" + if [[ -z "$VALUE" ]]; then + error 1 "$LABEL must be provided." + fi + if [[ ! "$VALUE" =~ ^[0-9]+([KkMmGgTtPp])?$ ]]; then + error 1 "$LABEL must be a positive integer optionally followed by K, M, G, T, or P (example: 1G)." + fi + local BYTES=$(toBytes "$VALUE") + if [[ -z "$BYTES" || "$BYTES" -le 0 ]]; then + error 1 "$LABEL must be greater than zero." + fi +} + +function validateBinaryFlag() { + local VALUE="$1" + local LABEL="$2" + if [[ ! "$VALUE" =~ ^[01]$ ]]; then + error 1 "$LABEL must be either 0 or 1." + fi +} + +function validateRuntime() { + local VALUE="$1" + if [[ -z "$VALUE" ]]; then + return 0 + fi + if [[ ! "$VALUE" =~ ^[0-9]+(ms|s|m|h)$ ]]; then + error 1 "RUNTIME must match the fio time format (e.g., 500ms, 5s, 2m, 1h)." + fi +} + +function validateInteger() { + local VALUE="$1" + local LABEL="$2" + local ALLOW_ZERO="${3:-0}" + local REGEX='^[1-9][0-9]*$' + local ERROR_MSG="$LABEL must be a positive integer." + + if [[ "$ALLOW_ZERO" -eq 1 ]]; then + REGEX='^[0-9]+$' + ERROR_MSG="$LABEL must be a non-negative integer." + fi + + if [[ -z "$VALUE" ]]; then + error 1 "$LABEL must be provided." + fi + if [[ ! "$VALUE" =~ $REGEX ]]; then + error 1 "$ERROR_MSG" + fi +} + +function ensureWritableTarget() { + local PATH_TO_CHECK="$1" + if [[ "$PATH_TO_CHECK" == "/" ]]; then + error 1 "Refusing to run against the filesystem root. Please set TARGET to a dedicated directory." + fi + if [[ -d "$PATH_TO_CHECK" && ! -w "$PATH_TO_CHECK" ]]; then + error 1 "TARGET directory is not writable: $PATH_TO_CHECK" + fi +} + function toBytes() { local SIZE=$1 local UNIT=${SIZE//[0-9]/} local NUMBER=${SIZE//[a-zA-Z]/} case $UNIT in + P|p) echo $((NUMBER * 1024 * 1024 * 1024 * 1024 * 1024));; T|t) echo $((NUMBER * 1024 * 1024 * 1024 * 1024));; G|g) echo $((NUMBER * 1024 * 1024 * 1024));; M|m) echo $((NUMBER * 1024 * 1024));; @@ -124,7 +237,7 @@ function parseRandomWriteResult() { function loadDefaultProfile() { NAME=("SEQ1MQ8T1" "SEQ1MQ1T1" "RND4KQ32T1" "RND4KQ1T1") LABEL=("Sequential 1M Q8T1" "Sequential 1M Q1T1" "Random 4K Q32T1" "Random 4K Q1T1") - COLOR=($(color $NORMAL $YELLOW) $(color $NORMAL $YELLOW) $(color $NORMAL $CYAN) $(color $NORMAL $CYAN)) + JOBCOLOR=($(color $NORMAL $YELLOW) $(color $NORMAL $YELLOW) $(color $NORMAL $CYAN) $(color $NORMAL $CYAN)) BLOCKSIZE=("1M" "1M" "4K" "4K") IODEPTH=(8 1 32 1) NUMJOBS=(1 1 1 1) @@ -135,7 +248,7 @@ function loadDefaultProfile() { function loadNVMeProfile() { NAME=("SEQ1MQ8T1" "SEQ128KQ32T1" "RND4KQ32T16" "RND4KQ1T1") LABEL=("Sequential 1M Q8T1" "Sequential 128K Q32T1" "Random 4K Q32T16" "Random 4K Q1T1") - COLOR=($(color $NORMAL $YELLOW) $(color $NORMAL $GREEN) $(color $NORMAL $CYAN) $(color $NORMAL $CYAN)) + JOBCOLOR=($(color $NORMAL $YELLOW) $(color $NORMAL $GREEN) $(color $NORMAL $CYAN) $(color $NORMAL $CYAN)) BLOCKSIZE=("1M" "128K" "4K" "4K") IODEPTH=(8 32 32 1) NUMJOBS=(1 1 16 1) @@ -165,16 +278,49 @@ function loadJob() { NAME=($JOB) LABEL="$READWRITELABEL $BLOCKSIZE Q${IODEPTH}T${NUMJOBS}" - COLOR=($(color $NORMAL $MAGENTA)) + JOBCOLOR=($(color $NORMAL $MAGENTA)) } +requireCommand fio +requireCommand dd +requireCommand awk +requireCommand df +if [[ -n "$JOB" ]]; then + requireCommand perl +fi + TARGET="${TARGET:-$(pwd)}" +ensureWritableTarget "$TARGET" if [ ! -d "$TARGET" ]; then ISNEWDIR=1 mkdir -p "$TARGET" fi + +validateSizeString "${SIZE:-1G}" "SIZE" +validateBinaryFlag "${WARMUP:-0}" "WARMUP" +validateBinaryFlag "${DRY_RUN:-0}" "DRY_RUN" +if [[ -n "$WARMUP_SIZE" ]]; then + validateSizeString "$WARMUP_SIZE" "WARMUP_SIZE" +fi +if [[ -n "$LOOPS" ]]; then + validateInteger "$LOOPS" "LOOPS" +fi +if [[ -n "$RUNTIME" ]]; then + validateRuntime "$RUNTIME" +fi DRIVELABEL="Drive" -FILESYSTEMPARTITION=$(lsblk -P | grep "$TARGET" | head -n 1 | awk '{print $1}' | cut -d"=" -f2 | cut -d"\"" -f2) + +FILESYSTEMPARTITION="" +if command -v lsblk &> /dev/null; then + FILESYSTEMPARTITION=$(lsblk -P 2>/dev/null | grep "$TARGET" | head -n 1 | awk '{print $1}' | cut -d"=" -f2 | cut -d"\"" -f2) +fi +if [ -z "$FILESYSTEMPARTITION" ] && command -v findmnt &> /dev/null; then + FILESYSTEMPARTITION=$(findmnt -n -o SOURCE "$TARGET" 2>/dev/null | sed 's|/dev/||') +fi +if [ -z "$FILESYSTEMPARTITION" ]; then + FILESYSTEMPARTITION=$(df "$TARGET" 2>/dev/null | tail +2 | awk '{print $1}' | sed 's|/dev/||') +fi + FILESYSTEMTYPE=$(df -T "$TARGET" | tail +2 | awk '{print $2}') FILESYSTEMSIZE=$(df -Th "$TARGET" | tail +2 | awk '{print $3}') ISOVERLAY=0 @@ -236,18 +382,17 @@ else DRIVENAME="Unknown" DRIVESIZE="Unknown" fi +if [ "$DRIVE" = "Unknown" ]; then + DRIVEINFO="Unknown" +else + DRIVEINFO="$DRIVENAME ($DRIVE, $DRIVESIZE) $DRIVEDETAILS" +fi if [ ! -z $JOB ]; then PROFILE="Job \"$JOB\"" loadJob else case "$PROFILE" in - default) - loadDefaultProfile - ;; - nvme) - loadNVMeProfile - ;; - *) + ""|auto) if [ $ISNVME -eq 1 ]; then PROFILE="auto (nvme)" loadNVMeProfile @@ -256,32 +401,66 @@ else loadDefaultProfile fi ;; + default) + loadDefaultProfile + ;; + nvme) + loadNVMeProfile + ;; + *) + error 1 "Invalid PROFILE: $(color $BOLD $WHITE)$PROFILE$(color $RESET). Allowed values are 'auto', 'default', or 'nvme'." + ;; esac fi case "$IO" in + ""|direct) + IO="direct (synchronous)" + DIRECT=1 + ;; buffered) IO="buffered (asynchronous)" DIRECT=0 ;; *) - IO="direct (synchronous)" - DIRECT=1 + error 1 "Invalid IO mode: $(color $BOLD $WHITE)$IO$(color $RESET). Allowed values are 'direct' or 'buffered'." ;; esac case "$DATA" in + ""|random|rand) + DATA="random" + WRITEZERO=0 + ;; zero | 0 | 0x00) DATA="zero (0x00)" WRITEZERO=1 ;; *) - DATA="random" - WRITEZERO=0 + error 1 "Invalid DATA pattern: $(color $BOLD $WHITE)$DATA$(color $RESET). Allowed values are 'random' or 'zero'." ;; esac SIZE="${SIZE:-1G}" BYTESIZE=$(toBytes $SIZE) WARMUP="${WARMUP:-0}" -if [ ! -z $LOOPS ]; then +if [ -z "$WARMUP_SIZE" ]; then + case "$PROFILE" in + *nvme*) WARMUP_SIZE="64M" ;; + *) WARMUP_SIZE="8M" ;; + esac +fi +validateSizeString "$WARMUP_SIZE" "WARMUP_SIZE" +WARMUP_BLOCK_BYTES=$(toBytes $WARMUP_SIZE) +if [ -z "$WARMUP_BLOCK_BYTES" ] || [ "$WARMUP_BLOCK_BYTES" -le 0 ]; then + WARMUP_BLOCK_BYTES=$(toBytes 8M) + WARMUP_SIZE="8M" +fi +BLOCK_MB=$((WARMUP_BLOCK_BYTES / 1024 / 1024)) +[ "$BLOCK_MB" -lt 1 ] && BLOCK_MB=1 +[ "$BLOCK_MB" -gt 1024 ] && BLOCK_MB=1024 + +if [[ -n "$LOOPS" ]] && [[ -n "$RUNTIME" ]]; then + LIMIT="Loops: $LOOPS (max $RUNTIME each)" + LIMIT_OPTION="--loops=$LOOPS --runtime=$RUNTIME" +elif [[ -n "$LOOPS" ]]; then LIMIT="Loops: $LOOPS" LIMIT_OPTION="--loops=$LOOPS" else @@ -292,16 +471,23 @@ fi echo -e "$(color $BOLD $WHITE)Configuration:$(color $RESET) - Target: $TARGET - - $DRIVELABEL: $DRIVENAME ($DRIVE, $DRIVESIZE) $DRIVEDETAILS + - $DRIVELABEL: $DRIVEINFO - Filesystem: $FILESYSTEMTYPE ($FILESYSTEMPARTITION, $FILESYSTEMSIZE) - Profile: $PROFILE - I/O: $IO - Data: $DATA - Size: $SIZE - - Warmup: $WARMUP + - Warmup: $WARMUP$([ "$WARMUP" -eq 1 ] && echo " (block: ${BLOCK_MB}M)") - $LIMIT +" -The benchmark is $(color $BOLD $WHITE)running$(color $RESET), please wait..." +DRY_RUN="${DRY_RUN:-0}" +if [ "$DRY_RUN" -eq 1 ]; then + echo -e "$SYM_SUCCESS Dry run $(color $BOLD $GREEN)completed$(color $RESET). Configuration is valid." + exit 0 +fi + +echo -e "The benchmark is $(color $BOLD $WHITE)running$(color $RESET), please wait..." fio_benchmark() { fio --filename="$TARGET/.diskmark.tmp" \ @@ -317,18 +503,46 @@ if [ $WARMUP -eq 1 ]; then else FILESOURCE=/dev/urandom fi - dd if="$FILESOURCE" of="$TARGET/.diskmark.tmp" bs="$BYTESIZE" count=1 oflag=direct + TOTAL_MB=$((BYTESIZE / 1024 / 1024)) + if [ "$TOTAL_MB" -eq 0 ]; then + dd if="$FILESOURCE" of="$TARGET/.diskmark.tmp" bs="$BYTESIZE" count=1 oflag=direct status=none + else + CHUNKS=$((TOTAL_MB / BLOCK_MB)) + REMAINDER_MB=$((TOTAL_MB % BLOCK_MB)) + if [ $CHUNKS -gt 0 ]; then + dd if="$FILESOURCE" of="$TARGET/.diskmark.tmp" bs=${BLOCK_MB}M count=$CHUNKS oflag=direct status=none + fi + if [ $REMAINDER_MB -gt 0 ]; then + dd if="$FILESOURCE" of="$TARGET/.diskmark.tmp" bs=1M count=$REMAINDER_MB oflag=direct conv=notrunc seek=$((CHUNKS * BLOCK_MB)) status=none + fi + fi fi +SKIPPED_JOBS=() + for ((i = 0; i < ${#NAME[@]}; i++)); do - TESTSIZE=$((${BYTESIZE} / ${SIZEDIVIDER[$i]:-1})) + DIVIDER=${SIZEDIVIDER[$i]:-1} + if [ "$DIVIDER" -le 0 ]; then + TESTSIZE=$BYTESIZE + else + TESTSIZE=$((BYTESIZE / DIVIDER)) + fi + BLOCKSIZE_BYTES=$(toBytes "${BLOCKSIZE[$i]}") + + if [ "$TESTSIZE" -lt "$BLOCKSIZE_BYTES" ]; then + SKIPPED_JOBS+=("${NAME[$i]} (size $(fromBytes $TESTSIZE) < block size ${BLOCKSIZE[$i]})") + echo + echo -e "${JOBCOLOR[$i]}${LABEL[$i]}:$(color $RESET) Skipped" + continue + fi + case "${READWRITE[$i]}" in rand) PARSE="parseRandom" ;; *) PARSE="parse" ;; esac echo - echo -e "${COLOR[$i]}${LABEL[$i]}:$(color $RESET)" + echo -e "${JOBCOLOR[$i]}${LABEL[$i]}:$(color $RESET)" printf "<= Read: " fio_benchmark "$TESTSIZE" "${NAME[$i]}Read" "${BLOCKSIZE[$i]}" "${IODEPTH[$i]}" "${NUMJOBS[$i]}" "${READWRITE[$i]}read" echo "$(${PARSE}ReadResult "${NAME[$i]}Read")" @@ -337,6 +551,13 @@ for ((i = 0; i < ${#NAME[@]}; i++)); do echo "$(${PARSE}WriteResult "${NAME[$i]}Write")" done -echo -e "\n✅ The benchmark is $(color $BOLD $GREEN)finished$(color $RESET)." +if [ ${#SKIPPED_JOBS[@]} -gt 0 ]; then + echo -e "\n$SYM_SUCCESS The benchmark is $(color $BOLD $GREEN)finished$(color $RESET) with $(color $BOLD $YELLOW)warnings$(color $RESET):" + for job in "${SKIPPED_JOBS[@]}"; do + echo -e " - $job" + done +else + echo -e "\n$SYM_SUCCESS The benchmark is $(color $BOLD $GREEN)finished$(color $RESET)." +fi clean