diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..d2f3bd5 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,34 @@ +codecov: + require_ci_to_pass: yes + notify: + wait_for_ci: yes + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + target: 45% + threshold: 1% + base: auto + patch: + default: + target: 80% + threshold: 5% + +comment: + layout: "reach,diff,flags,tree,footer" + behavior: default + require_changes: no + require_base: no + require_head: yes + +ignore: + - "cmd/**/*" + - "examples/**/*" + - "server/**/*" + - "testing/**/*" + - "**/*_test.go" + - "*.md" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 307796b..f2663c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,17 +2,120 @@ name: CI on: push: - branches: [ master ] + branches: [ master, main, develop ] pull_request: - branches: [ master ] + branches: [ master, main, develop ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - test: - name: Test + # Quick validation - fail fast on obvious issues + validate: + name: Quick Validation runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Download dependencies + run: go mod download && go mod verify + + - name: Check formatting + run: | + if [ "$(gofmt -s -l . | grep -v vendor | wc -l)" -gt 0 ]; then + echo "Code formatting issues found:" + gofmt -s -d . | grep -v vendor + exit 1 + fi + + - name: Lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + args: --timeout=5m --fix=false + + # Test on primary Go version + test: + name: Test (Go 1.23) + runs-on: ubuntu-latest + needs: validate + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-1.23-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-1.23- + + - name: Download dependencies + run: go mod download + + - name: Run tests with coverage + run: go test -v -race -covermode=atomic -coverprofile=coverage.out ./... + + - name: Generate coverage report + run: go tool cover -html=coverage.out -o coverage.html + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.out + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + + - name: Archive coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: | + coverage.out + coverage.html + retention-days: 30 + + # Test on multiple Go versions (after primary test passes) + test-matrix: + name: Test (Go ${{ matrix.go-version }}) + runs-on: ${{ matrix.os }} + needs: test strategy: + fail-fast: true # Stop on first failure matrix: - go-version: ['1.21', '1.22', '1.23'] + os: [ubuntu-latest, macos-latest, windows-latest] + go-version: ['1.21', '1.22'] + exclude: + - os: macos-latest + go-version: '1.21' + - os: windows-latest + go-version: '1.21' steps: - name: Checkout code @@ -26,46 +129,53 @@ jobs: - name: Cache Go modules uses: actions/cache@v4 with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} restore-keys: | - ${{ runner.os }}-go- + ${{ runner.os }}-go-${{ matrix.go-version }}- - name: Download dependencies run: go mod download - name: Run tests - run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - file: ./coverage.txt - flags: unittests - name: codecov-umbrella + run: go test -v -race ./... - lint: - name: Lint + # Code quality - only run if tests pass + sonarcloud: + name: Code Quality (SonarCloud) runs-on: ubuntu-latest + needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 with: - go-version: '1.23' + fetch-depth: 0 - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v8 + - name: Download coverage from test job + uses: actions/download-artifact@v4 with: - version: v2.2 - args: --timeout=5m ./cmd/onvif-cli ./cmd/onvif-quick ./cmd/onvif-server ./discovery/... ./internal/... . + name: coverage-report + + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + -Dsonar.projectKey=0x524a_go-onvif + -Dsonar.organization=0x524a + -Dsonar.go.coverage.reportPaths=coverage.out + # Build verification build: name: Build runs-on: ubuntu-latest + needs: test steps: - name: Checkout code @@ -76,7 +186,18 @@ jobs: with: go-version: '1.23' - - name: Build + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-1.23-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-1.23- + + - name: Download dependencies + run: go mod download + + - name: Build main packages run: go build -v ./... - name: Build examples diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..d0551b2 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,42 @@ +name: Additional Coverage Reports + +on: + workflow_run: + workflows: [CI] + types: [completed] + branches: [master, main] + +jobs: + # Generate additional coverage analysis if CI passed + coverage-analysis: + name: Coverage Analysis + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: coverage-report + + - name: Check coverage percentage + run: | + if [ -f coverage.out ]; then + coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') + echo "Coverage: $coverage%" + # Set threshold to 40% + if (( $(echo "$coverage < 40" | bc -l) )); then + echo "⚠️ Coverage below 40% threshold: $coverage%" + else + echo "✅ Coverage above threshold: $coverage%" + fi + fi + + - name: Upload coverage badge + continue-on-error: true + run: | + # Optional: Update badges or notifications + echo "Coverage analysis complete" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..72a177d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Extra Tests + +on: + workflow_dispatch: # Manual trigger only + schedule: + - cron: '0 2 * * *' # Daily at 2 AM UTC + +jobs: + # Run tests on other Go versions as manual/scheduled job + test-older-versions: + name: Test on Go ${{ matrix.go-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + go-version: ['1.20', '1.19'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go-version }}- + + - name: Download dependencies + run: go mod download + + - name: Run tests + run: go test -v -race ./... diff --git a/README.md b/README.md index e5942ec..7d2d77c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/0x524a/onvif-go.svg)](https://pkg.go.dev/github.com/0x524a/onvif-go) [![Go Report Card](https://goreportcard.com/badge/github.com/0x524a/onvif-go)](https://goreportcard.com/report/github.com/0x524a/onvif-go) +[![codecov](https://codecov.io/gh/0x524a/onvif-go/branch/master/graph/badge.svg)](https://codecov.io/gh/0x524a/onvif-go) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=0x524a_go-onvif&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=0x524a_go-onvif) [![License](https://img.shields.io/github/license/0x524a/onvif-go)](LICENSE) [![GitHub stars](https://img.shields.io/github/stars/0x524a/onvif-go)](https://github.com/0x524a/onvif-go/stargazers) [![GitHub issues](https://img.shields.io/github/issues/0x524a/onvif-go)](https://github.com/0x524a/onvif-go/issues) diff --git a/device_additional_test.go b/device_additional_test.go index f76a94e..c3e051d 100644 --- a/device_additional_test.go +++ b/device_additional_test.go @@ -17,14 +17,14 @@ func newMockDeviceAdditionalServer() *httptest.Server { Content []byte `xml:",innerxml"` } `xml:"Body"` } - decoder.Decode(&envelope) + _ = decoder.Decode(&envelope) bodyContent := string(envelope.Body.Content) w.Header().Set("Content-Type", "application/soap+xml") switch { case strings.Contains(bodyContent, "GetGeoLocation"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -38,7 +38,7 @@ func newMockDeviceAdditionalServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetGeoLocation"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -46,7 +46,7 @@ func newMockDeviceAdditionalServer() *httptest.Server { `)) case strings.Contains(bodyContent, "DeleteGeoLocation"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -54,7 +54,7 @@ func newMockDeviceAdditionalServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetDPAddresses"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -71,7 +71,7 @@ func newMockDeviceAdditionalServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetDPAddresses"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -79,7 +79,7 @@ func newMockDeviceAdditionalServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetAccessPolicy"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -92,7 +92,7 @@ func newMockDeviceAdditionalServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetAccessPolicy"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -100,7 +100,7 @@ func newMockDeviceAdditionalServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetWsdlUrl"): - w.Write([]byte(` + _, _ = w.Write([]byte(` diff --git a/device_certificates_test.go b/device_certificates_test.go index d0edad7..f4391c9 100644 --- a/device_certificates_test.go +++ b/device_certificates_test.go @@ -14,8 +14,8 @@ func newMockDeviceCertificatesServer() *httptest.Server { w.Header().Set("Content-Type", "application/soap+xml") // Parse request to determine which operation - buf := make([]byte, r.ContentLength) - r.Body.Read(buf) + buf := make([]byte, r.ContentLength) + _, _ = r.Body.Read(buf) requestBody := string(buf) var response string diff --git a/device_extended_test.go b/device_extended_test.go index cbc3759..f30dec8 100644 --- a/device_extended_test.go +++ b/device_extended_test.go @@ -17,14 +17,14 @@ func newMockDeviceExtendedServer() *httptest.Server { Content []byte `xml:",innerxml"` } `xml:"Body"` } - decoder.Decode(&envelope) + _ = decoder.Decode(&envelope) bodyContent := string(envelope.Body.Content) w.Header().Set("Content-Type", "application/soap+xml") switch { case strings.Contains(bodyContent, "AddScopes"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -32,7 +32,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "RemoveScopes"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -42,7 +42,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetScopes"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -50,7 +50,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetRelayOutputs"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -66,7 +66,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetRelayOutputSettings"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -74,7 +74,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetRelayOutputState"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -82,7 +82,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SendAuxiliaryCommand"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -92,7 +92,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetSystemLog"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -104,7 +104,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetSystemFactoryDefault"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -112,7 +112,7 @@ func newMockDeviceExtendedServer() *httptest.Server { `)) case strings.Contains(bodyContent, "StartFirmwareUpgrade"): - w.Write([]byte(` + _, _ = w.Write([]byte(` diff --git a/device_security_test.go b/device_security_test.go index a40dc95..9164d62 100644 --- a/device_security_test.go +++ b/device_security_test.go @@ -17,14 +17,14 @@ func newMockDeviceSecurityServer() *httptest.Server { Content []byte `xml:",innerxml"` } `xml:"Body"` } - decoder.Decode(&envelope) + _ = decoder.Decode(&envelope) bodyContent := string(envelope.Body.Content) w.Header().Set("Content-Type", "application/soap+xml") switch { case strings.Contains(bodyContent, "GetRemoteUser"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -38,7 +38,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetRemoteUser"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -46,7 +46,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetIPAddressFilter"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -64,7 +64,7 @@ func newMockDeviceSecurityServer() *httptest.Server { case strings.Contains(bodyContent, "SetIPAddressFilter"), strings.Contains(bodyContent, "AddIPAddressFilter"), strings.Contains(bodyContent, "RemoveIPAddressFilter"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -72,7 +72,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetZeroConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -86,7 +86,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetZeroConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -94,7 +94,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetPasswordComplexityConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -109,7 +109,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetPasswordComplexityConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -117,7 +117,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetPasswordHistoryConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -128,7 +128,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetPasswordHistoryConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -136,7 +136,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "GetAuthFailureWarningConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -148,7 +148,7 @@ func newMockDeviceSecurityServer() *httptest.Server { `)) case strings.Contains(bodyContent, "SetAuthFailureWarningConfiguration"): - w.Write([]byte(` + _, _ = w.Write([]byte(` diff --git a/device_storage_test.go b/device_storage_test.go index 9841f6f..7aa18ca 100644 --- a/device_storage_test.go +++ b/device_storage_test.go @@ -13,8 +13,8 @@ func newMockDeviceStorageServer() *httptest.Server { w.Header().Set("Content-Type", "application/soap+xml") // Parse request to determine which operation - buf := make([]byte, r.ContentLength) - r.Body.Read(buf) + buf := make([]byte, r.ContentLength) + _, _ = r.Body.Read(buf) requestBody := string(buf) var response string diff --git a/device_wifi_test.go b/device_wifi_test.go index b93e4aa..9fe7cf3 100644 --- a/device_wifi_test.go +++ b/device_wifi_test.go @@ -13,8 +13,8 @@ func newMockDeviceWiFiServer() *httptest.Server { w.Header().Set("Content-Type", "application/soap+xml") // Parse request to determine which operation - buf := make([]byte, r.ContentLength) - r.Body.Read(buf) + buf := make([]byte, r.ContentLength) + _, _ = r.Body.Read(buf) requestBody := string(buf) var response string diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..69b4347 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,29 @@ +sonar.projectKey=0x524a_go-onvif +sonar.organization=0x524a + +# Project metadata +sonar.projectName=go-onvif +sonar.projectVersion=1.0.0 + +# Source code location +sonar.sources=. +sonar.exclusions=**/vendor/**,**/*_test.go,**/examples/**,**/cmd/**,**/server/**,**/testing/** + +# Test settings +sonar.tests=. +sonar.test.inclusions=**/*_test.go +sonar.test.exclusions=**/vendor/** + +# Go specific settings +sonar.language=go +sonar.go.coverage.reportPaths=coverage.out +sonar.go.tests.reportPaths=test-report.json + +# Source encoding +sonar.sourceEncoding=UTF-8 + +# Coverage exclusions +sonar.coverage.exclusions=**/cmd/**,**/examples/**,**/server/**,**/testing/**,**/*_test.go + +# Duplications +sonar.cpd.exclusions=**/*_test.go