diff --git a/.github/workflows/go-snippets-pr-check.yaml b/.github/workflows/go-snippets-pr-check.yaml new file mode 100644 index 000000000..25a91b8c8 --- /dev/null +++ b/.github/workflows/go-snippets-pr-check.yaml @@ -0,0 +1,84 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Go Snippets Build on PR and Schedule + +on: + # Run on pull requests targeting the main branch. + pull_request: + branches: [ "main" ] + + # Run on a weekly schedule (every Sunday at 3:00 AM UTC). + schedule: + - cron: '0 3 * * 0' + +jobs: + # This job builds and runs the Go snippets. + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Fetch the entire history to compare branches in the PR. + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + # --- PR-Specific Checks --- + # These steps only run for pull request events. + + - name: Find new, un-tested Go files (PR only) + if: github.event_name == 'pull_request' + run: | + echo "Checking for any new Go files that are not in the build script..." + ./tools/go-snippets/check_go_snippets.sh + + - name: Get changed Go snippet files (PR only) + if: github.event_name == 'pull_request' + id: changed-files + run: | + # This command gets the list of changed .go files in the snippets directory + # between the PR's head and the base branch. + # The output is an escaped, space-separated string suitable for shell commands. + CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} -- 'examples/go/snippets/**/*.go' | xargs) + echo "changed_files=${CHANGED_FILES}" >> $GITHUB_OUTPUT + echo "Detected changes in: ${CHANGED_FILES}" + + - name: Build changed Go snippets (PR only) + if: github.event_name == 'pull_request' && steps.changed-files.outputs.changed_files != '' + run: | + echo "Building only the changed Go files..." + ./tools/go-snippets/runner.sh build ${{ steps.changed-files.outputs.changed_files }} + + # --- Scheduled Checks --- + # These steps only run on the weekly schedule. + + - name: Build all Go snippets (Scheduled) + if: github.event_name == 'schedule' + run: | + echo "Running weekly build for all Go snippets..." + ./tools/go-snippets/runner.sh build diff --git a/examples/go/go.mod b/examples/go/go.mod new file mode 100644 index 000000000..8cea5b1fd --- /dev/null +++ b/examples/go/go.mod @@ -0,0 +1,44 @@ +module snippets + +go 1.25.1 + +require ( + google.golang.org/adk v0.1.0 + google.golang.org/genai v1.35.0 +) + +require ( + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/a2aproject/a2a-go v0.3.0 // indirect + github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/jsonschema-go v0.3.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + rsc.io/omap v1.2.0 // indirect + rsc.io/ordered v1.1.1 // indirect +) diff --git a/examples/go/go.sum b/examples/go/go.sum new file mode 100644 index 000000000..9ada95150 --- /dev/null +++ b/examples/go/go.sum @@ -0,0 +1,90 @@ +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/a2aproject/a2a-go v0.3.0 h1:mnfBEDJXShzEhXCmUbfZ9xo8sXfq2pCxemsY9uasvzg= +github.com/a2aproject/a2a-go v0.3.0/go.mod h1:8C0O6lsfR7zWFEqVZz/+zWCoxe8gSWpknEpqm/Vgj3E= +github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= +github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/adk v0.1.0 h1:+w/fHuqRVolotOATlujRA+2DKUuDrFH2poRdEX2QjB8= +google.golang.org/adk v0.1.0/go.mod h1:NvtSLoNx7UzZIiUAI1KoJQLMmt9sG3oCgiCx1TLqKFw= +google.golang.org/genai v1.35.0 h1:Jo6g25CzVqFzGrX5mhWyBgQqXAUzxcx5jeK7U74zv9c= +google.golang.org/genai v1.35.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genproto v0.0.0-20251014184007-4626949a642f h1:vLd1CJuJOUgV6qijD7KT5Y2ZtC97ll4dxjTUappMnbo= +google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f h1:OiFuztEyBivVKDvguQJYWq1yDcfAHIID/FVrPR4oiI0= +google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f/go.mod h1:kprOiu9Tr0JYyD6DORrc4Hfyk3RFXqkQ3ctHEum3ZbM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= +rsc.io/omap v1.2.0/go.mod h1:C8pkI0AWexHopQtZX+qiUeJGzvc8HkdgnsWK4/mAa00= +rsc.io/ordered v1.1.1 h1:1kZM6RkTmceJgsFH/8DLQvkCVEYomVDJfBRLT595Uak= +rsc.io/ordered v1.1.1/go.mod h1:evAi8739bWVBRG9aaufsjVc202+6okf8u2QeVL84BCM= diff --git a/tools/go-snippets/README.md b/tools/go-snippets/README.md new file mode 100644 index 000000000..4234d2429 --- /dev/null +++ b/tools/go-snippets/README.md @@ -0,0 +1,80 @@ +# Go Snippets Tooling + +This directory contains the scripts and configuration for building, running, and testing the Go snippets located in `examples/go/`. + +## Overview + +The tooling is designed to ensure that all Go snippets are continuously validated and to provide a fast feedback loop for developers. It consists of a unified runner script, a configuration file to manage the list of snippets, and a suite of unit tests for the runner itself. + +## Key Components + +- **`runner.sh`**: The main script for building and running Go snippets. +- **`files_to_test.txt`**: The configuration file that lists all Go snippets to be tested. +- **`check_go_snippets.sh`**: A PR check script that ensures all `.go` files are registered in `files_to_test.txt`. +- **`runner_test.sh`**: Unit tests for the `runner.sh` script. + +--- + +## How to Use + +### Automatic Execution (CI/CD) + +The scripts are primarily designed to be run automatically by GitHub Actions. + +- **On Pull Requests:** When a pull request is opened, two workflows are triggered: + 1. **Go Snippets Build on PR and Schedule:** This workflow runs `check_go_snippets.sh` to ensure new files are registered. It then intelligently builds **only the `.go` files that were changed** in the PR. + 2. **Go Build and Test on PR:** This workflow runs a full build of **all** Go snippets and executes any unit tests (`go test ./...`) to ensure that a change has not broken any other part of the Go codebase. + +- **Scheduled Runs:** A full regression build of all Go snippets is run automatically every Sunday at 3:00 AM UTC to catch any potential issues. + +### Manual Execution + +You can also run the scripts locally to test your changes before pushing. All commands should be run from the **root of the repository**. + +#### Building All Snippets + +To run a full build of every Go snippet listed in `files_to_test.txt`: + +```bash +./tools/go-snippets/runner.sh build +``` + +#### Building Specific Snippets + +To build one or more specific Go snippets (for example, if you are working on them and want a quick check): + +```bash +./tools/go-snippets/runner.sh build examples/go/snippets/quickstart/main.go +``` + +#### Running the Unit Tests + +To run the unit tests for the `runner.sh` script itself: + +```bash +./tools/go-snippets/runner_test.sh +``` + +--- + +## Maintaining the Snippet List + +### Adding a New Snippet + +1. Create your new `.go` file (e.g., `examples/go/snippets/my-new-snippet/main.go`). +2. Open `tools/go-snippets/files_to_test.txt`. +3. Add a new line with the path to your file, relative to the `examples/go/` directory. + + ``` + # In files_to_test.txt + snippets/my-new-snippet/main.go + ``` + +4. If your snippet is part of a package that requires multiple files to be built together, add them all to the same line: + + ``` + # In files_to_test.txt + snippets/my-multi-file-snippet/main.go snippets/my-multi-file-snippet/helpers.go + ``` + +The `check_go_snippets.sh` script will automatically run on your PR and remind you if you've forgotten to add your new file to the list. diff --git a/tools/go-snippets/check_go_snippets.sh b/tools/go-snippets/check_go_snippets.sh new file mode 100755 index 000000000..7243324e5 --- /dev/null +++ b/tools/go-snippets/check_go_snippets.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script ensures that every .go file within the Go snippets directory +# is referenced in the files_to_test.txt file. This prevents new snippets +# from being added without being included in the regression test suite. + +# --- Configuration --- +RED='\033[0;31m' +NC='\033[0m' # No Color +EXIT_CODE=0 +SNIPPETS_FILE="tools/go-snippets/files_to_test.txt" + +# --- Logic --- +echo "Checking for Go files that are not registered in ${SNIPPETS_FILE}..." + +# Find all .go files in the snippets directory, excluding _test.go files. +all_go_files=$(find examples/go -type f -name "*.go" ! -name "*_test.go" | sed 's|examples/go/||' | sort) + +# Extract all .go file paths from the snippets file, ignoring comments and arguments. +referenced_files=$(grep -v '^\s*#' "${SNIPPETS_FILE}" | grep -o '[a-zA-Z0-9/._-]*\.go' | sort | uniq) + +# Compare the list of all .go files with the list of referenced files. +unreferenced_files=$(comm -23 <(echo "${all_go_files}") <(echo "${referenced_files}")) + +if [[ -n "${unreferenced_files}" ]]; then + echo -e "${RED}Error: The following Go files were found but are not referenced in ${SNIPPETS_FILE}:${NC}" + # Indent the list of files for readability. + echo "${unreferenced_files}" | sed 's/^/ /' + echo + echo "Please add them to ${SNIPPETS_FILE} to include them in the regression tests." + EXIT_CODE=1 +else + echo "All Go files are correctly referenced in the snippets file." +fi + +# Check for files in the list that don't exist on disk +dangling_references=$(comm -23 <(echo "${referenced_files}") <(echo "${all_go_files}")) + +if [[ -n "${dangling_references}" ]]; then + echo -e "${RED}Error: The following files are referenced in ${SNIPPETS_FILE} but do not exist:${NC}" + echo "${dangling_references}" | sed 's/^/ /' + echo + echo "Please remove them from ${SNIPPETS_FILE}." + EXIT_CODE=1 +fi + +exit ${EXIT_CODE} diff --git a/tools/go-snippets/files_to_test.txt b/tools/go-snippets/files_to_test.txt new file mode 100644 index 000000000..631c38cdb --- /dev/null +++ b/tools/go-snippets/files_to_test.txt @@ -0,0 +1,46 @@ +# This file lists Go snippet files to be built and run by runner.sh. +# Each line represents a single build/run target. +# +# Format: +# - Lines starting with '#' are treated as comments and ignored. +# - Each non-comment line should contain one or more Go file paths, +# relative to the 'examples/go/' directory. +# - If multiple files are on a single line, they are treated as part of the same package +# and built/run together (e.g., for files with shared dependencies). +# - Arguments are not supported +# +# Examples: +# # Single file example +# snippets/quickstart/main.go +# +# # Multiple files in a single package example +# snippets/tools-custom/doc_analysis/main.go snippets/tools-custom/doc_analysis/doc_analysis.go + +cloud-run/main.go +a2a_basic/remote_a2a/check_prime_agent/main.go +a2a_basic/main.go +snippets/tools-custom/doc_analysis/main.go snippets/tools-custom/doc_analysis/doc_analysis.go +snippets/tools-custom/order_status/main.go snippets/tools-custom/order_status/order_status.go +snippets/tools-custom/user_preference/main.go snippets/tools-custom/user_preference/user_preference.go +snippets/tools-custom/weather_sentiment/main.go +snippets/callbacks/types_of_callbacks/main.go +snippets/context/main.go +snippets/tools/function-tools/func_tool.go +snippets/sessions/state_example/state_example.go +snippets/sessions/session_management_example/session_management_example.go +snippets/sessions/instruction_template/instruction_template_example.go +snippets/sessions/instruction_provider/instruction_provider_example.go +snippets/artifacts/main.go +snippets/tools/built-in-tools/google_search.go +snippets/callbacks/main.go +snippets/agents/models/models.go +snippets/agents/custom-agent/storyflow_agent.go +snippets/agents/workflow-agents/sequential/main.go +snippets/agents/workflow-agents/parallel/main.go +snippets/agents/workflow-agents/loop/main.go +snippets/agents/multi-agent/main.go +snippets/tools/function-tools/long-running-tool/long_running_tool.go +snippets/agents/llm-agents/snippets/main.go +snippets/agents/llm-agents/main.go +snippets/sessions/memory_example/memory_example.go +snippets/tools-custom/customer_support_agent/main.go \ No newline at end of file diff --git a/tools/go-snippets/runner.sh b/tools/go-snippets/runner.sh new file mode 100755 index 000000000..a130396e6 --- /dev/null +++ b/tools/go-snippets/runner.sh @@ -0,0 +1,182 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script builds and runs Go snippets. It is designed to be run from the project root. +# +# It can run in two modes: +# 1. Targeted Mode: If file paths are provided as arguments, it runs only those files. +# This is used in PR checks to test only the changed files. +# Example: ./tools/go-snippets/runner.sh build examples/go/snippets/quickstart/main.go +# +# 2. Full Regression Mode: If no arguments are provided, it runs a predefined +# list of all Go snippets in the repository. This is used for scheduled weekly tests. +# Example: ./tools/go-snippets/runner.sh build + +# --- Configuration --- +# Define color codes for colored output. +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Global exit code for the script. It is set to 1 if any test fails. +EXIT_CODE=0 + +# The configuration file that lists all Go snippets to be tested. +SNIPPETS_FILE="tools/go-snippets/files_to_test.txt" + +# --- Helper Functions --- + +# should_process_line determines if a line from the snippets file should be processed. +# It returns 0 (success) for valid lines and 1 (failure) for comments or empty lines. +# +# @param {string} line - The line to check. +# @returns {int} 0 if the line should be processed, 1 otherwise. +should_process_line() { + local line=$1 + # Remove all whitespace from the line to correctly handle lines with only spaces or tabs. + local trimmed_line=$(echo "${line}" | tr -d '[:space:]') + # Return failure (1) if the trimmed line is empty or starts with a hash. + if [[ -z "${trimmed_line}" || "${trimmed_line}" =~ ^# ]]; then + return 1 + else + return 0 + fi +} + +# find_snippet_line searches the SNIPPETS_FILE for a given Go file path. +# It ignores comments and returns the full line from the file. +# +# @param {string} file_path_from_root - The full path to the Go file relative to the project root (e.g., "examples/go/snippets/quickstart/main.go"). +# @returns {string} The matching line from SNIPPETS_FILE, or an empty string if not found. +find_snippet_line() { + local file_path_from_root=$1 + # The SNIPPETS_FILE contains paths relative to 'examples/go/', so we strip that prefix from the input path. + local relative_path=${file_path_from_root#examples/go/} + # First, filter out all commented lines, then search for the relative path. + grep -v '^\s*#' "${SNIPPETS_FILE}" | grep "${relative_path}" +} + +# get_command_for_action constructs the appropriate Go command based on the action. +# It specifically handles stripping arguments for the 'build' action. +# +# @param {string} action - The action to perform ("build" or "run"). +# @param {string} line - The line from the snippets file, which may include arguments. +# @returns {string} The fully formed Go command. +get_command_for_action() { + local action=$1 + local line=$2 + local command="" + + if [ "${action}" == "build" ]; then + # For 'build', extract only the file paths, ignoring any arguments. + # 'go build' does not accept application arguments, so they must be stripped. + local files_to_build=$(echo "${line}" | awk '{for(i=1;i<=NF;i++) if($i ~ /\.go$/) printf "%s ", $i}') + command="go build ${files_to_build}" + elif [ "${action}" == "run" ]; then + # For 'run', use the line as is, as 'go run' will pass arguments to the application. + command="go run ${line}" + fi + echo "${command}" +} + +# execute_and_check executes a command and prints a formatted status message. +# +# @param {string} command - The full command to execute. +# @param {string} display_name - A user-friendly name for the command/file. +execute_and_check() { + local command=$1 + local display_name=$2 + + # 'eval' is used to correctly execute the command string, which may contain quotes and other special characters. + local output + output=$(eval ${command} 2>&1) + local exit_code=$? + + if [ ${exit_code} -eq 0 ]; then + echo -e "[${GREEN}PASS${NC}] ${display_name}" + else + echo -e "[${RED}FAIL${NC}] ${display_name}" + # Indent the error output for better readability. + echo "${output}" | sed 's/^/ /' + # Set the global exit code to indicate failure. + EXIT_CODE=1 + fi +} + +# --- Main Logic --- + +# This check prevents the main logic from running if the script is being sourced (e.g., by the test script). +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # Validate the first argument is either 'build' or 'run'. + if [[ "$1" != "build" && "$1" != "run" ]]; then + echo "Usage: $0 [file1 file2 ...]" + exit 1 + fi + + ACTION=$1 + shift # Remove the first argument, so '$@' contains only the file paths. + + # Ensure all Go module dependencies are tidy before running any builds or tests. + # This is run from the 'examples/go' directory where the go.mod file is located. + (cd examples/go && go mod tidy) + if [ $? -ne 0 ]; then + echo -e "[${RED}FAIL${NC}] go mod tidy failed in examples/go" + exit 1 # Exit immediately if dependencies are not clean. + fi + + # Check if file paths were provided as arguments (Targeted Mode). + if [ "$#" -gt 0 ]; then + echo "Running targeted Go snippet ${ACTION} for changed files..." + echo + for file in "$@"; do + # Find the corresponding line in the snippets file for the changed file. + line=$(find_snippet_line "${file}") + if [[ -z "${line}" ]]; then + echo -e "[${RED}FAIL${NC}] ${file}" + echo " Error: No corresponding entry found in ${SNIPPETS_FILE}." + EXIT_CODE=1 + continue # Skip to the next file. + fi + + # Construct the appropriate build or run command. + command_to_execute=$(get_command_for_action "${ACTION}" "${line}") + if [[ -n "${command_to_execute}" ]]; then + # Execute the command from the 'examples/go' directory. + execute_and_check "(cd examples/go && ${command_to_execute})" "${file}" + fi + done + else + # If no file paths were provided, run in Full Regression Mode. + echo "Running full Go snippet ${ACTION}..." + echo + # Read the snippets file line by line. + while IFS= read -r line; do + # Skip empty lines and comments. + if ! should_process_line "${line}"; then + continue + fi + + command_to_execute=$(get_command_for_action "${ACTION}" "${line}") + if [[ -n "${command_to_execute}" ]]; then + execute_and_check "(cd examples/go && ${command_to_execute})" "${line}" + fi + done < "${SNIPPETS_FILE}" + fi + + echo + echo "Script finished." + # Exit with the final status code (0 for success, 1 for failure). + exit ${EXIT_CODE} +fi diff --git a/tools/go-snippets/runner_test.sh b/tools/go-snippets/runner_test.sh new file mode 100755 index 000000000..fa6696924 --- /dev/null +++ b/tools/go-snippets/runner_test.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Unit tests for runner.sh. +# This script can be run directly to validate the logic of the functions in runner.sh. + +# --- Test Setup --- +# Source the script to make its functions available for testing. +source "$(dirname "$0")/runner.sh" + +# --- Test Harness --- +# Simple assertion function to check for equality. +assert_equals() { + local expected=$1 + local actual=$2 + local test_name=$3 + + if [ "${expected}" == "${actual}" ]; then + echo -e "[${GREEN}PASS${NC}] ${test_name}" + else + echo -e "[${RED}FAIL${NC}] ${test_name}" + echo " Expected: '${expected}'" + echo " Actual: '${actual}'" + exit 1 + fi +} + +# --- Test Cases --- + +# Tests that the 'run' action correctly forms a 'go run' command, +# preserving any arguments that might be on the line. +test_get_command_for_run_action() { + local line="snippets/quickstart/main.go" + local expected="go run snippets/quickstart/main.go" + local actual=$(get_command_for_action "run" "${line}") + assert_equals "${expected}" "${actual}" "Should create correct 'run' command without arguments" +} + +# Tests that the 'build' action correctly forms a 'go build' command +# and, most importantly, strips any non-.go file arguments from the line. +# This is critical because 'go build' does not accept application arguments. +test_get_command_for_build_action_strips_args() { + local line="snippets/quickstart/main.go" + local expected="go build snippets/quickstart/main.go " + local actual=$(get_command_for_action "build" "${line}") + assert_equals "${expected}" "${actual}" "Should create correct 'build' command and strip arguments" +} + +# Tests that a line with multiple .go files is correctly handled for a build. +# This is important for packages that are split across multiple files. +test_get_command_for_multi_file_build() { + local line="file1.go file2.go" + local expected="go build file1.go file2.go " + local actual=$(get_command_for_action "build" "${line}") + assert_equals "${expected}" "${actual}" "Should handle multiple files correctly for build" +} + +# Tests that a line with multiple .go files is correctly handled for a run. +test_get_command_for_multi_file_run() { + local line="file1.go file2.go" + local expected="go run file1.go file2.go" + local actual=$(get_command_for_action "run" "${line}") + assert_equals "${expected}" "${actual}" "Should handle multiple files correctly for run" +} + +# Tests the core logic for finding a snippet in the configuration file. +# It ensures that: +# 1. The 'examples/go/' prefix is correctly stripped from the input path. +# 2. The correct line is found in the test file. +# 3. Commented-out lines are ignored. +test_find_snippet_line() { + # Create a temporary SNIPPETS_FILE for this test to isolate it. + local original_snippets_file="${SNIPPETS_FILE}" + SNIPPETS_FILE=$(mktemp) + # Populate the temporary file with test data. + # This first line acts as a negative test case. The test specifically searches for 'snippets/quickstart/main.go', + # so this line should be correctly ignored by the grep logic, ensuring the function doesn't just return the first line it finds. + echo "file1.go" > "${SNIPPETS_FILE}" + # This line is a commented-out version of our target and should also be ignored. + echo "# snippets/quickstart/main.go" >> "${SNIPPETS_FILE}" # This line should be ignored by grep. + # This is the actual line we expect the function to find and return. + echo "snippets/quickstart/main.go" >> "${SNIPPETS_FILE}" # This is the line we expect to find. + + # This simulates the file path that would be passed to the script (e.g., from 'git diff'). + local input_file_path="examples/go/snippets/quickstart/main.go" + local expected_line="snippets/quickstart/main.go" + + # Call the function under test. + local actual_line=$(find_snippet_line "${input_file_path}") + + assert_equals "${expected_line}" "${actual_line}" "Should find the correct snippet line, ignoring comments" + + # Cleanup + rm "${SNIPPETS_FILE}" + SNIPPETS_FILE="${original_snippets_file}" # Restore original path +} + +# Tests the logic for determining if a line should be processed. +test_should_process_line() { + # A valid line should return 0 (success). + should_process_line "file1.go" + assert_equals "0" "$?" "Should return success for a valid line" + + # An empty line should return 1 (failure). + should_process_line "" + assert_equals "1" "$?" "Should return failure for an empty line" + + # A line with only whitespace should return 1 (failure). + should_process_line " " + assert_equals "1" "$?" "Should return failure for a whitespace-only line" + + # A commented line should return 1 (failure). + should_process_line "# file1.go" + assert_equals "1" "$?" "Should return failure for a commented line" + + # A commented line with leading spaces should return 1 (failure). + should_process_line " # file1.go" + assert_equals "1" "$?" "Should return failure for a commented line with leading spaces" +} + +# --- Run Tests --- +echo "Running tests for runner.sh..." +test_get_command_for_run_action +test_get_command_for_build_action_strips_args +test_get_command_for_multi_file_build +test_get_command_for_multi_file_run +test_find_snippet_line +test_should_process_line +echo "All tests passed."