pax_global_header00006660000000000000000000000064151103677610014521gustar00rootroot0000000000000052 comment=a6352138aa161706548d999b53eb0c5b499f7e2f go-snaps-0.5.16/000077500000000000000000000000001511036776100133415ustar00rootroot00000000000000go-snaps-0.5.16/.github/000077500000000000000000000000001511036776100147015ustar00rootroot00000000000000go-snaps-0.5.16/.github/FUNDING.yml000066400000000000000000000000261511036776100165140ustar00rootroot00000000000000github: [gkampitakis] go-snaps-0.5.16/.github/ISSUE_TEMPLATE/000077500000000000000000000000001511036776100170645ustar00rootroot00000000000000go-snaps-0.5.16/.github/ISSUE_TEMPLATE/bug.yml000066400000000000000000000015031511036776100203630ustar00rootroot00000000000000name: šŸ› Bug report description: Something doesn't work šŸ”„ title: '[Bug]: ' labels: ['bug'] body: - type: textarea id: description attributes: label: Description description: A clear and concise description of what the bug is. validations: required: true - type: textarea id: steps-to-reproduce attributes: label: Steps to Reproduce description: | List of steps, sample code, screenshot(if visual bug), or a link to code or a project that reproduces the behavior. The more details the faster will be to respond and potentially resolve the issue validations: required: true - type: textarea id: expected-behavior attributes: label: Expected Behavior description: A clear and concise description of what you expected to happen. go-snaps-0.5.16/.github/ISSUE_TEMPLATE/feature.yml000066400000000000000000000011371511036776100212440ustar00rootroot00000000000000name: šŸš€ Feature Proposal description: Submit a proposal for a new feature title: '[Feature Request]: ' labels: ['enhancement'] body: - type: textarea id: proposal attributes: label: šŸš€ Feature Proposal description: A clear and concise description of what the feature is. validations: required: true - type: textarea id: motivation attributes: label: Motivation description: The motivation for the proposal. - type: textarea id: example attributes: label: Example description: An example for how this feature would be used. go-snaps-0.5.16/.github/ISSUE_TEMPLATE/misc.yml000066400000000000000000000003411511036776100205400ustar00rootroot00000000000000name: ā” Other description: Open an issue that is not feature or bug related body: - type: textarea id: text attributes: label: Issue description: Give as much detail as you can to help us understand. go-snaps-0.5.16/.github/workflows/000077500000000000000000000000001511036776100167365ustar00rootroot00000000000000go-snaps-0.5.16/.github/workflows/go.yml000066400000000000000000000022211511036776100200630ustar00rootroot00000000000000name: Go on: pull_request: paths-ignore: - "images/**" - "**/*.md" branches: - main push: paths-ignore: - "images/**" - "**/*.md" branches: - main jobs: lint: name: Run linting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: "go.mod" - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: version: "latest" - name: Format lint run: | make install-tools && make format && git diff && git diff --quiet test: name: Run tests runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] steps: - name: Set git to use LF run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@v4 - name: Setup go uses: actions/setup-go@v5 with: go-version-file: "go.mod" - name: Run Tests run: make test - name: Run Tests run: make test-trimpath go-snaps-0.5.16/.gitignore000066400000000000000000000004411511036776100153300ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) vendor/ **/.DS_Store .vscode go-snaps-0.5.16/.golangci.yml000066400000000000000000000006361511036776100157320ustar00rootroot00000000000000version: "2" linters: enable: - lll - revive - staticcheck - unconvert - unparam disable: - errcheck settings: lll: line-length: 130 revive: rules: - name: exported disabled: true exclusions: paths: - internal/difflib/difflib.go formatters: enable: - goimports - gofumpt settings: gofumpt: extra-rules: true go-snaps-0.5.16/LICENSE000066400000000000000000000020641511036776100143500ustar00rootroot00000000000000MIT License Copyright (c) 2021 Georgios Kampitakis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-snaps-0.5.16/Makefile000066400000000000000000000014411511036776100150010ustar00rootroot00000000000000.PHONY: install-tools lint test test-verbose format help help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' install-tools: ## Install linting tools go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install mvdan.cc/gofumpt@latest go install github.com/segmentio/golines@latest lint: ## Run golangci linter golangci-lint run ./... format: ## Format code gofumpt -l -w -extra . golines . -w test: ## Run tests go test -race -count=10 -shuffle on -cover ./... test-verbose: ## Run tests with verbose output go test -race -count=10 -shuffle on -v -cover ./... test-trimpath: ## Run tests with -trimpath GOFLAGS=-trimpath go test -race -count=10 -shuffle on -v -cover ./examples go-snaps-0.5.16/README.md000066400000000000000000000371441511036776100146310ustar00rootroot00000000000000# Go Snaps [![Go](https://github.com/gkampitakis/go-snaps/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/gkampitakis/go-snaps/actions/workflows/go.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/gkampitakis/go-snaps)](https://goreportcard.com/report/github.com/gkampitakis/go-snaps) [![Go Reference](https://pkg.go.dev/badge/github.com/gkampitakis/go-snaps.svg)](https://pkg.go.dev/github.com/gkampitakis/go-snaps)

Jest-like snapshot testing in Go


Logo

## Contents - [Installation](#installation) - [MatchSnapshot](#matchsnapshot) - [MatchStandaloneSnapshot](#matchstandalonesnapshot) - [MatchJSON](#matchjson) - [MatchStandaloneJSON](#matchstandalonejson) - [MatchYAML](#matchyaml) - [MatchStandaloneYAML](#matchstandaloneyaml) - [MatchInlineSnapshot](#matchinlinesnapshot) `Experimental` - [Matchers](#matchers) - [match.Any](#matchany) - [match.Custom](#matchcustom) - [match.Type\[ExpectedType\]](#matchtype) - [Configuration](#configuration) - [Update Snapshots](#update-snapshots) - [Clean obsolete Snapshots](#clean-obsolete-snapshots) - [Sort Snapshots](#sort-snapshots) - [Skipping Tests](#skipping-tests) - [Running tests on CI](#running-tests-on-ci) - [No Color](#no-color) - [Snapshots Structure](#snapshots-structure) - [Known Limitations](#known-limitations) - [Acknowledgments](#acknowledgments) - [Contributing](./contributing.md) ## Installation To install `go-snaps`, use `go get`: ```bash go get github.com/gkampitakis/go-snaps ``` Import the `go-snaps/snaps` package into your code: ```go package example import ( "testing" "github.com/gkampitakis/go-snaps/snaps" ) func TestExample(t *testing.T) { snaps.MatchSnapshot(t, "Hello World") } ``` ## MatchSnapshot `MatchSnapshot` can be used to capture any type of data structured or unstructured. You can pass multiple parameters to `MatchSnapshot` or call `MatchSnapshot` multiple times inside the same test. The difference is in the latter, it will create multiple entries in the snapshot file. ```go // test_simple.go func TestSimple(t *testing.T) { t.Run("should make multiple entries in snapshot", func(t *testing.T) { snaps.MatchSnapshot(t, 5, 10, 20, 25) snaps.MatchSnapshot(t, "some value") }) } ``` `go-snaps` saves the snapshots in `__snapshots__` directory and the file name is the test file name with extension `.snap`. So for example if your test is called `test_simple.go` when you run your tests, a snapshot file will be created at `./__snapshots__/test_simple.snaps`. ## MatchStandaloneSnapshot `MatchStandaloneSnapshot` will create snapshots on separate files as opposed to `MatchSnapshot` which adds multiple snapshots inside the same file. _Combined with `snaps.Ext` you can have proper syntax highlighting and better readability_ ```go // test_simple.go func TestSimple(t *testing.T) { snaps.MatchStandaloneSnapshot(t, "Hello World") // or create an html snapshot file snaps.WithConfig(snaps.Ext(".html")). MatchStandaloneSnapshot(t, "

Hello World

") } ``` `go-snaps` saves the snapshots in `__snapshots__` directory and the file name is the `t.Name()` plus a number plus the extension `.snap`. So for the above example the snapshot file name will be `./__snapshots__/TestSimple_1.snap` and `./__snapshots__/TestSimple_1.snap.html`. ## MatchJSON `MatchJSON` can be used to capture data that can represent a valid json. You can pass a valid json in form of `string` or `[]byte` or whatever value can be passed successfully on `json.Marshal`. ```go func TestJSON(t *testing.T) { type User struct { Age int Email string } snaps.MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`) snaps.MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`)) snaps.MatchJSON(t, User{10, "mock-email"}) } ``` JSON will be saved in snapshot in pretty format for more readability and deterministic diffs. ## MatchStandaloneJSON `MatchStandaloneJSON` will create snapshots on separate files as opposed to `MatchJSON` which adds multiple snapshots inside the same file. ```go func TestSimple(t *testing.T) { snaps.MatchStandaloneJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`) snaps.MatchStandaloneJSON(t, User{10, "mock-email"}) } ``` `go-snaps` saves the snapshots in `__snapshots__` directory and the file name is the `t.Name()` plus a number plus the extension `.snap.json`. So for the above example the snapshot file name will be `./__snapshots__/TestSimple_1.snap.json` and `./__snapshots__/TestSimple_2.snap.json`. ## MatchYAML `MatchYAML` can be used to capture data that can represent a valid yaml. You can pass a valid json in form of `string` or `[]byte` or whatever value can be passed successfully on `yaml.Marshal`. ```go func TestYAML(t *testing.T) { type User struct { Age int Email string } snaps.MatchYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com") snaps.MatchYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com")) snaps.MatchYAML(t, User{10, "mock-email"}) } ``` ## MatchStandaloneYAML `MatchStandaloneYAML` will create snapshots on separate files as opposed to `MatchYAML` which adds multiple snapshots inside the same file. ```go func TestSimple(t *testing.T) { snaps.MatchStandaloneYAML(t, "user: \"mock-user\"\nage: 10\nemail: \"mock@email.com\"") snaps.MatchStandaloneYAML(t, User{10, "mock-email"}) } ``` `go-snaps` saves the snapshots in `__snapshots__` directory and the file name is the `t.Name()` plus a number plus the extension `.snap.yaml`. So for the above example the snapshot file name will be `./__snapshots__/TestSimple_1.snap.yaml` and `./__snapshots__/TestSimple_2.snap.yaml`. ### Matchers `MatchJSON`'s and `MatchYAML`'s third argument can accept a list of matchers. Matchers are functions that can act as property matchers and test values. You can pass the path of the property you want to match and test. Currently `go-snaps` has three build in matchers - `match.Any` - `match.Custom` - `match.Type[ExpectedType]` _Open to feedback for building more matchers or you can build your own [example](./examples/matchJSON_test.go#L16)._ #### Path Syntax For JSON go-snaps utilises gjson. _More information about the supported path syntax from [gjson](https://github.com/tidwall/gjson/blob/v1.17.0/SYNTAX.md)._ As for YAML go-snaps utilises [github.com/goccy/go-yaml#5-use-yamlpath](https://github.com/goccy/go-yaml#5-use-yamlpath). _More information about the supported syntax [PathString](https://github.com/goccy/go-yaml/blob/9cbf5d4217830fd4ad1504e9ed117c183ade0994/path.go#L17-L26)._ #### match.Any Any matcher acts as a placeholder for any value. It replaces any targeted path with a placeholder string. ```go Any("user.name") // or with multiple paths Any("user.name", "user.email") ``` Any matcher provides some methods for setting options ```go match.Any("user.name"). Placeholder(value). // allows to define a different placeholder value from the default "" ErrOnMissingPath(bool) // determines whether the matcher will err in case of a missing, default true ``` #### match.Custom Custom matcher allows you to bring your own validation and placeholder value ```go match.Custom("user.age", func(val any) (any, error) { age, ok := val.(float64) if !ok { return nil, fmt.Errorf("expected number but got %T", val) } return "some number", nil }) ``` The callback parameter value for JSON can be on of these types: ```go bool // for JSON booleans float64 // for JSON numbers string // for JSON string literals nil // for JSON null map[string]any // for JSON objects []any // for JSON arrays ``` If Custom matcher returns an error the snapshot test will fail with that error. Custom matcher provides a method for setting an option ```go match.Custom("path",myFunc). Placeholder(value). // allows to define a different placeholder value from the default "" ErrOnMissingPath(bool) // determines whether the matcher will err in case of a missing path, default true ``` #### match.Type Type matcher evaluates types that are passed in a snapshot and it replaces any targeted path with a placeholder in the form of ``. ```go match.Type[string]("user.info") // or with multiple paths match.Type[float64]("user.age", "data.items") ``` Type matcher provides a method for setting an option ```go match.Type[string]("user.info"). ErrOnMissingPath(bool) // determines whether the matcher will err in case of a missing path, default true ``` You can see more [examples](./examples/matchJSON_test.go#L96). ## MatchInlineSnapshot `MatchInlineSnapshot` allows you to store expected snapshot values directly within your test source code, rather than in external snapshot files. **First run** - Create the snapshot by passing `nil` on the last argument: ```go func TestInlineSnapshot(t *testing.T) { snaps.MatchInlineSnapshot(t, "Hello World", nil) snaps.MatchInlineSnapshot(t, 123, nil) snaps.MatchInlineSnapshot(t, map[string]int{"foo": 1, "bar": 2}, nil) } ``` Then run your tests, and `go-snaps` will automatically insert the snapshot into your test code. ```go func TestInlineSnapshot(t *testing.T) { snaps.MatchInlineSnapshot(t, "Hello World", snaps.Inline("Hello World")) snaps.MatchInlineSnapshot(t, 123, snaps.Inline("int(123)")) snaps.MatchInlineSnapshot(t, map[string]int{"foo": 1, "bar": 2}, snaps.Inline(`map[string]int{"bar":2, "foo":1}`)) } ``` Every subsequent test run will compare the actual value against the inline snapshot, and if they differ, the test will fail and display a diff. > [!NOTE] > `MatchInlineSnapshot` is experimental and looking for feedback and thorough testing before being marked as stable. ## Configuration `go-snaps` allows passing configuration for overriding - the directory where snapshots are stored, _relative or absolute path_ - the filename where snapshots are stored - the snapshot file's extension (_regardless the extension the filename will include the `.snaps` inside the filename_) - programmatically control whether to update snapshots. _You can find an example usage at [examples](/examples/examples_test.go#13)_ - json config's json format configuration: - `Width`: The maximum width in characters before wrapping json output (default: 80) - `Indent`: The indentation string to use for nested structures (default: 1 spaces) - `SortKeys`: Whether to sort json object keys alphabetically (default: true) ```go t.Run("snapshot tests", func(t *testing.T) { snaps.WithConfig(snaps.Filename("my_custom_name"), snaps.Dir("my_dir")).MatchSnapshot(t, "Hello Word") s := snaps.WithConfig( snaps.Dir("my_dir"), snaps.Filename("json_file"), snaps.Ext(".json"), snaps.Update(false), snaps.JSON(snaps.JSONConfig{ Width: 80, Indent: " ", SortKeys: false, }), ) s.MatchJSON(t, `{"hello":"world"}`) }) ``` You can see more on [examples](/examples/matchSnapshot_test.go#L67) ## Update Snapshots You can update your failing snapshots by setting `UPDATE_SNAPS` env variable to true. ```bash UPDATE_SNAPS=true go test ./... ``` If you don't want to update all failing snapshots, or you want to update only one of them you can you use the `-run` flag to target the test(s) you want. For more information on `go test` flags you can run ```go go help testflag ``` ### Clean obsolete snapshots

Summary Obsolete Summary Removed

`go-snaps` can identify obsolete snapshots. In order to enable this functionality you need to use `TestMain(m *testing.M)` to call `snaps.Clean(t)` after your tests have run. This will also print a **Snapshot Summary**. (if running tests with verbose flag `-v`) If you want to remove the obsolete snap files and snapshots you can run tests with `UPDATE_SNAPS=clean` env variable. The reason for using `TestMain` is because `go-snaps` needs to be sure that all tests are finished so it can keep track of which snapshots were not called. **Example:** ```go func TestMain(m *testing.M) { v := m.Run() // After all tests have run `go-snaps` can check for unused snapshots snaps.Clean(m) // dirty, err := snaps.Clean(m) // _ = err // if dirty { // fmt.Println("Some snapshots were outdated.") // os.Exit(1) // } os.Exit(v) } ``` For more information around [TestMain](https://pkg.go.dev/testing#hdr-Main). ### Sort Snapshots By default `go-snaps` appends new snaps to the snapshot file and in case of parallel tests the order is random. If you want snaps to be sorted in deterministic order you need to use `TestMain` per package: ```go func TestMain(m *testing.M) { v := m.Run() // After all tests have run `go-snaps` will sort snapshots snaps.Clean(m, snaps.CleanOpts{Sort: true}) os.Exit(v) } ``` ### Skipping Tests If you want to skip one test using `t.Skip`, `go-snaps` can't keep track if the test was skipped or if it was removed. For that reason `go-snaps` exposes a wrapper for `t.Skip`, `t.Skipf` and `t.SkipNow`, which keep tracks of skipped files. You can skip, or only run specific tests by using the `-run` flag. `go-snaps` can identify which tests are being skipped and parse only the relevant tests for obsolete snapshots. ## Running Tests on CI When `go-snaps` detects that it is running in CI it will automatically fail when snapshots are missing or there diffs. This is done to ensure new snapshots are committed alongside the tests and assertions are successful. You can override this behavior by setting `UPDATE_SNAPS` to `always` when running your tests that will create or update snapshots. > `go-snaps` uses [ciinfo](https://github.com/gkampitakis/ciinfo) for detecting if it runs on CI environment. ## No Color `go-snaps` supports disabling color outputs by running your tests with the env variable `NO_COLOR` set to any value. ```bash NO_COLOR=true go test ./... ``` For more information around [NO_COLOR](https://no-color.org). ## Snapshots Structure Snapshots have the form ```text [TestName - Number] --- ``` `TestID` is the test name plus an increasing number to allow multiple calls of `MatchSnapshot` in a single test. ```txt [TestSimple/should_make_a_map_snapshot - 1] map[string]any{ "mock-0": "value", "mock-1": int(2), "mock-2": func() {...}, "mock-3": float32(10.399999618530273), } --- ``` > [!NOTE] > If your snapshot data contain characters `---` at the start of a line followed by a new line, `go-snaps` will "escape" them and save them as `/-/-/-/` to differentiate them from termination characters. ## Known Limitations - When running a specific test file by specifying a path `go test ./my_test.go`, `go-snaps` can't track the path so it will mistakenly mark snapshots as obsolete. - go-snaps doesn't handle CRLF line endings. If you are using Windows, you may need to convert the line endings to LF. - go-snaps cannot determine the snapshot path automatically when running with `go test -trimpath ./...`. It then instead relies on the current working directory to define the snapshot directory. If this is a problem in your use case you can set an absolute path with `snaps.WithConfig(snaps.Dir("/some/absolute/path"))` ## Acknowledgments This library used [Jest Snapshoting](https://jestjs.io/docs/snapshot-testing) and [Cupaloy](https://github.com/bradleyjkemp/cupaloy) as inspiration. - Jest is a full-fledged Javascript testing framework and has robust snapshoting features. - Cupaloy is a great and simple Golang snapshoting solution. - The [logo](https://github.com/MariaLetta/free-gophers-pack) was made by [MariaLetta](https://github.com/MariaLetta). go-snaps-0.5.16/contributing.md000066400000000000000000000012701511036776100163720ustar00rootroot00000000000000 # Contributing go-snaps is a pet open-source project. That means there are no any sorts of guarantees but I am happy to review any contribution to help the project improve. ## Running Locally Running the project locally is straight forward. You will need `go>=1.16` version installed. In the project there is an `examples` folder where you can experiment and test how `go-snaps` works. You can run the tests with ```go go test ./examples/... -v -race -count=1 ``` ## Creating a pr Before making a pull request make sure your changes are linted and formatted else GH actions will fail. In the root of the project there is a `Makefile`. You can run `make help` for the list of commands. go-snaps-0.5.16/examples/000077500000000000000000000000001511036776100151575ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__/000077500000000000000000000000001511036776100177755ustar00rootroot00000000000000TestMatchStandaloneJSON_matchers_Any_matcher_should_ignore_fields_1.snap.json000077500000000000000000000001251511036776100370400ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "age": 10, "nested": { "now": [ "" ] }, "user": "mock-user" }TestMatchStandaloneJSON_matchers_Custom_matcher_JSON_string_validation_1.snap.json000077500000000000000000000001211511036776100377410ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "age": "", "email": "mock@email.com", "user": "mock-user" }TestMatchStandaloneJSON_matchers_Custom_matcher_struct_marshalling_1.snap.json000077500000000000000000000001401511036776100372760ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "email": "mock-user@email.com", "keys": [ 1, 2, 3, 4, 5 ], "name": "mock-user" }78d60ee26285b161eb01a6e2031017213de31244.paxheader00006660000000000000000000000227151103677610020105xustar00rootroot00000000000000151 path=go-snaps-0.5.16/examples/__snapshots__/TestMatchStandaloneJSON_matchers_Type_matcher_should_create_snapshot_with_type_placeholder_1.snap.json 78d60ee26285b161eb01a6e2031017213de31244.data000077500000000000000000000000351511036776100167430ustar00rootroot00000000000000{ "data": "" }a96108fdc24343ff1b899fd4d35c28506fad0ce1.paxheader00006660000000000000000000000227151103677610020607xustar00rootroot00000000000000151 path=go-snaps-0.5.16/examples/__snapshots__/TestMatchStandaloneJSON_matchers_Type_matcher_should_create_snapshot_with_type_placeholder_2.snap.json a96108fdc24343ff1b899fd4d35c28506fad0ce1.data000077500000000000000000000000611511036776100174440ustar00rootroot00000000000000{ "metadata": "" }TestMatchStandaloneJSON_should_create_a_prettyJSON_snap_1.snap.json000077500000000000000000000001001511036776100347050ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "age": 10, "email": "mock@email.com", "user": "mock-user" }TestMatchStandaloneJSON_should_create_a_prettyJSON_snap_2.snap.json000077500000000000000000000001001511036776100347060ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "age": 10, "email": "mock@email.com", "user": "mock-user" }TestMatchStandaloneJSON_should_create_a_prettyJSON_snap_following_by_config_1.snap.json000077500000000000000000000001111511036776100410060ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "user": "mock-user", "age": 10, "email": "mock@email.com" }TestMatchStandaloneJSON_should_create_a_prettyJSON_snap_following_by_config_2.snap.json000077500000000000000000000001111511036776100410070ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "user": "mock-user", "age": 10, "email": "mock@email.com" }TestMatchStandaloneJSON_should_make_a_json_object_snapshot_1.snap.json000077500000000000000000000001351511036776100355430ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__{ "mock-0": "value", "mock-1": 2, "mock-2": { "Msg": "Hello World" }, "mock-3": 10.4 }go-snaps-0.5.16/examples/__snapshots__/TestMatchStandaloneJSON_should_marshal_struct_1.snap.json000077500000000000000000000001331511036776100331400ustar00rootroot00000000000000{ "email": "mock@email.com", "keys": [ 1, 2, 3, 4, 5 ], "name": "mock-name" }TestMatchStandaloneSnapshot_should_create_html_snapshots_1.snap.html000077500000000000000000000001471511036776100354450ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__

My First Heading

My first paragraph.

TestMatchStandaloneSnapshot_should_create_html_snapshots_2.snap.html000077500000000000000000000000261511036776100354420ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__
Hello World
TestMatchStandaloneYAML_matchers_Any_matcher_should_ignore_fields_1.snap.yaml000077500000000000000000000000641511036776100370240ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__user: mock-user age: 10 nested: now: - TestMatchStandaloneYAML_matchers_Custom_matcher_YAML_string_validation_1.snap.yaml000077500000000000000000000000741511036776100377230ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__user: mock-user age: email: mock@email.comTestMatchStandaloneYAML_matchers_Custom_matcher_struct_marshalling_1.snap.yaml000077500000000000000000000001171511036776100372640ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__name: mock-name email: mock-user@email.com keys: - 1 - 2 - 3 - 4 - 5 d17195b3167a34fe3175a4e2dc556c9b2372c652.paxheader00006660000000000000000000000227151103677610020302xustar00rootroot00000000000000151 path=go-snaps-0.5.16/examples/__snapshots__/TestMatchStandaloneYAML_matchers_Type_matcher_should_create_snapshot_with_type_placeholder_1.snap.yaml d17195b3167a34fe3175a4e2dc556c9b2372c652.data000077500000000000000000000000231511036776100171350ustar00rootroot00000000000000data: 6c12a7018ab18ff6662268d2a2dee233f8d3014f.paxheader00006660000000000000000000000227151103677610020432xustar00rootroot00000000000000151 path=go-snaps-0.5.16/examples/__snapshots__/TestMatchStandaloneYAML_matchers_Type_matcher_should_create_snapshot_with_type_placeholder_2.snap.yaml 6c12a7018ab18ff6662268d2a2dee233f8d3014f.data000077500000000000000000000000511511036776100172660ustar00rootroot00000000000000metadata: TestMatchStandaloneYAML_should_make_a_yaml_object_snapshot_1.snap.yaml000077500000000000000000000001001511036776100355060ustar00rootroot00000000000000go-snaps-0.5.16/examples/__snapshots__mock-0: value mock-1: 2 mock-2: msg: Hello World mock-3: 10.4 go-snaps-0.5.16/examples/__snapshots__/TestMatchStandaloneYAML_should_marshal_struct_1.snap.yaml000077500000000000000000000001231511036776100331210ustar00rootroot00000000000000name: mock-name email: mock@email.com keys: - 1 - 2 - 3 - 4 - 5 tags: [] go-snaps-0.5.16/examples/__snapshots__/custom_file.snap000077500000000000000000000001251511036776100231720ustar00rootroot00000000000000 [TestMatchSnapshot/withConfig/should_allow_changing_filename - 1] snapshot data --- go-snaps-0.5.16/examples/__snapshots__/matchJSON_test.snap000077500000000000000000000033121511036776100235070ustar00rootroot00000000000000 [TestMatchJSON/should_make_a_json_object_snapshot - 1] { "mock-0": "value", "mock-1": 2, "mock-2": { "Msg": "Hello World" }, "mock-3": 10.4 } --- [TestMatchJSON/should_create_a_prettyJSON_snap - 1] { "age": 10, "email": "mock@email.com", "user": "mock-user" } --- [TestMatchJSON/should_create_a_prettyJSON_snap - 2] { "age": 10, "email": "mock@email.com", "user": "mock-user" } --- [TestMatchJSON/should_marshal_struct - 1] { "email": "mock@email.com", "keys": [ 1, 2, 3, 4, 5 ], "name": "mock-name" } --- [TestMatchers/Custom_matcher/struct_marshalling - 1] { "email": "mock-user@email.com", "keys": [ 1, 2, 3, 4, 5 ], "name": "mock-user" } --- [TestMatchers/Any_matcher/should_ignore_fields - 1] { "age": 10, "nested": { "now": [ "" ] }, "user": "mock-user" } --- [TestMatchers/my_matcher/should_allow_using_your_matcher - 1] { "value": "blue" } --- [TestMatchers/Custom_matcher/JSON_string_validation - 1] { "age": "", "email": "mock@email.com", "user": "mock-user" } --- [TestMatchers/Any_matcher/http_response - 1] { "data": { "createdAt": "", "message": "hello world" } } --- [TestMatchers/Type_matcher/should_create_snapshot_with_type_placeholder - 1] { "data": "" } --- [TestMatchers/Type_matcher/should_create_snapshot_with_type_placeholder - 2] { "metadata": "" } --- [TestMatchJSON/should_create_a_prettyJSON_snap_following_by_config - 1] { "user": "mock-user", "age": 10, "email": "mock@email.com" } --- [TestMatchJSON/should_create_a_prettyJSON_snap_following_by_config - 2] { "user": "mock-user", "age": 10, "email": "mock@email.com" } --- go-snaps-0.5.16/examples/__snapshots__/matchSnapshot_test.snap000077500000000000000000000047441511036776100245470ustar00rootroot00000000000000 [TestMatchSnapshot/should_make_an_int_snapshot - 1] int(5) --- [TestMatchSnapshot/should_make_a_string_snapshot - 1] string snapshot --- [TestMatchSnapshot/should_make_a_map_snapshot - 1] map[string]interface {}{ "mock-0": "value", "mock-1": int(2), "mock-2": func() {...}, "mock-3": float32(10.399999618530273), } --- [TestMatchSnapshot/should_make_multiple_entries_in_snapshot - 1] int(5) int(10) int(20) int(25) --- [TestMatchSnapshot/should_make_create_multiple_snapshot - 1] int(1000) --- [TestMatchSnapshot/should_make_create_multiple_snapshot - 2] another snapshot --- [TestMatchSnapshot/should_make_create_multiple_snapshot - 3] { "user": "gkampitakis", "id": 1234567, "data": [ ] } --- [TestMatchSnapshot/nest/more/one_more_nested_test - 1] it's okay --- [TestMatchSnapshot/.* - 1] ignore regex patterns on names --- [TestSimpleTable/string - 1] input --- [TestSimpleTable/integer - 1] int(10) --- [TestSimpleTable/map - 1] map[string]interface {}{ "test": func() {...}, } --- [TestSimpleTable/buffer - 1] &bytes.Buffer{ buf: {0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67}, off: 0, lastRead: 0, } --- [TestMatchSnapshot/withConfig - 1] this should use the default config --- [TestUpdateWithFlag/test_-_0 - 1] lore ipsum dolor sit amet --- [TestUpdateWithFlag/test_-_1 - 1] consectetur adipiscing elit --- [TestUpdateWithFlag/test_-_2 - 1] sed do eiusmod tempor incididunt ut labore et dolore magna aliqua --- [TestUpdateWithFlag/test_-_3 - 1] Ut enim ad minim veniam, quis nostrud laboris nisi ut aliquip ex ea commodo consequat. --- [TestParallel/should_snap_an_integer - 1] int(10) --- [TestParallel/should_snap_an_integer_slice - 1] []int{1, 2, 3, 4} --- [TestParallel/should_snap_a_struct - 1] struct { user string; email string; age int }{user:"gkampitakis", email:"mock@mail.com", age:10} --- [TestParallel/should_snap_a_float - 1] float64(10.5) --- [TestParallel/should_snap_a_buffer - 1] &bytes.Buffer{ buf: {0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67}, off: 0, lastRead: 0, } --- [TestParallel/should_snap_a_map - 1] map[string]int{"value-0":0, "value-1":1, "value-2":2, "value-3":3} --- [TestParallel/should_snap_a_struct_with_fields - 1] struct { _ struct {}; name string; id string }{ _: struct {}{}, name: "mock-name", id: "123456", } --- [TestParallel/should_snap_a_pointer - 1] &int(10) --- go-snaps-0.5.16/examples/__snapshots__/matchYAML_test.snap000077500000000000000000000006241511036776100235030ustar00rootroot00000000000000 [TestMatchYaml/should_match_struct_yaml - 1] name: John Doe age: 30 email: john.doe@example.com address: mock-address time: mock-time --- [TestMatchYaml/custom_matching_logic - 1] name: mock-user email: mock-user@email.com keys: - 1 - 2 - 3 - 4 - 5 --- [TestMatchYaml/type_matcher - 1] data: --- [TestMatchYaml/type_matcher - 2] metadata: --- go-snaps-0.5.16/examples/__snapshots__/my_standalone_snap_1.snap000077500000000000000000000000151511036776100247550ustar00rootroot00000000000000hello world-0go-snaps-0.5.16/examples/__snapshots__/my_standalone_snap_2.snap000077500000000000000000000000151511036776100247560ustar00rootroot00000000000000hello world-1go-snaps-0.5.16/examples/__snapshots__/my_standalone_snap_3.snap000077500000000000000000000000151511036776100247570ustar00rootroot00000000000000hello world-2go-snaps-0.5.16/examples/__snapshots__/my_standalone_snap_4.snap000077500000000000000000000000151511036776100247600ustar00rootroot00000000000000hello world-3go-snaps-0.5.16/examples/absolute_path/000077500000000000000000000000001511036776100200115ustar00rootroot00000000000000go-snaps-0.5.16/examples/absolute_path/matchSnapshot_test.snap000077500000000000000000000001341511036776100245500ustar00rootroot00000000000000 [TestMatchSnapshot/withConfig/should_allow_absolute_path - 1] supporting absolute path --- go-snaps-0.5.16/examples/matchInlineSnapshot_test.go000066400000000000000000000073351511036776100225300ustar00rootroot00000000000000package examples import ( "testing" "github.com/gkampitakis/go-snaps/snaps" ) func TestMatchInlineSnapshot(t *testing.T) { t.Run("should make an inline snapshot", func(t *testing.T) { u := struct { User string Age int }{ User: "mock-name", Age: 30, } snaps.MatchInlineSnapshot( t, u, snaps.Inline("struct { User string; Age int }{User:\"mock-name\", Age:30}"), ) }) t.Run("should create multiline inline snapshot", func(t *testing.T) { snaps.MatchInlineSnapshot(t, "line1\nline2\nline3", snaps.Inline(`line1 line2 line3`)) }) t.Run("should handle simple types", func(t *testing.T) { snaps.MatchInlineSnapshot(t, 42, snaps.Inline("int(42)")) snaps.MatchInlineSnapshot(t, 3.14159, snaps.Inline("float64(3.14159)")) snaps.MatchInlineSnapshot(t, true, snaps.Inline("bool(true)")) snaps.MatchInlineSnapshot(t, "hello", snaps.Inline("hello")) }) t.Run("should handle slices and arrays", func(t *testing.T) { snaps.MatchInlineSnapshot(t, []int{1, 2, 3}, snaps.Inline("[]int{1, 2, 3}")) snaps.MatchInlineSnapshot( t, []string{"a", "b", "c"}, snaps.Inline("[]string{\"a\", \"b\", \"c\"}"), ) }) t.Run("should handle maps", func(t *testing.T) { m := map[string]int{"foo": 1, "bar": 2} snaps.MatchInlineSnapshot(t, m, snaps.Inline(`map[string]int{"bar":2, "foo":1}`)) }) t.Run("should handle nested structures", func(t *testing.T) { type Address struct { Street string City string } type Person struct { Name string Age int Address Address } p := Person{ Name: "John Doe", Age: 25, Address: Address{ Street: "123 Main St", City: "Springfield", }, } snaps.MatchInlineSnapshot(t, p, snaps.Inline(`examples.Person{ Name: "John Doe", Age: 25, Address: examples.Address{Street:"123 Main St", City:"Springfield"}, }`)) }) t.Run("should handle pointers", func(t *testing.T) { val := 100 ptr := &val snaps.MatchInlineSnapshot(t, ptr, snaps.Inline("&int(100)")) }) t.Run("should handle empty values", func(t *testing.T) { snaps.MatchInlineSnapshot(t, "", snaps.Inline("")) snaps.MatchInlineSnapshot(t, []int{}, snaps.Inline("[]int{}")) snaps.MatchInlineSnapshot(t, map[string]int{}, snaps.Inline("map[string]int{}")) }) t.Run("should handle special characters in strings", func(t *testing.T) { snaps.MatchInlineSnapshot(t, "hello\tworld", snaps.Inline("hello world")) snaps.MatchInlineSnapshot(t, "line1\nline2", snaps.Inline(`line1 line2`)) snaps.MatchInlineSnapshot(t, "quotes: \"test\"", snaps.Inline("quotes: \"test\"")) snaps.MatchInlineSnapshot(t, `quotes: "test"`, snaps.Inline("quotes: \"test\"")) }) t.Run("should handle multiple inline snapshots in sequence", func(t *testing.T) { type Product struct { ID int Name string Price float64 } p1 := Product{ID: 1, Name: "Widget", Price: 9.99} p2 := Product{ID: 2, Name: "Gadget", Price: 19.99} snaps.MatchInlineSnapshot( t, p1, snaps.Inline("examples.Product{ID:1, Name:\"Widget\", Price:9.99}"), ) snaps.MatchInlineSnapshot( t, p2, snaps.Inline("examples.Product{ID:2, Name:\"Gadget\", Price:19.99}"), ) }) t.Run("nested tests with inline snapshots", func(t *testing.T) { t.Run("inner test 1", func(t *testing.T) { snaps.MatchInlineSnapshot(t, "nested-1", snaps.Inline("nested-1")) }) t.Run("inner test 2", func(t *testing.T) { snaps.MatchInlineSnapshot(t, "nested-2", snaps.Inline("nested-2")) }) }) t.Run("should handle complex multiline content", func(t *testing.T) { jsonLike := `{ "name": "test", "value": 123, "nested": { "key": "value" } }` snaps.MatchInlineSnapshot(t, jsonLike, snaps.Inline(`{ "name": "test", "value": 123, "nested": { "key": "value" } }`)) }) } go-snaps-0.5.16/examples/matchJSON_test.go000066400000000000000000000106761511036776100203450ustar00rootroot00000000000000package examples import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/gkampitakis/go-snaps/match" "github.com/gkampitakis/go-snaps/snaps" ) type myMatcher struct { age int } func Matcher() *myMatcher { return &myMatcher{} } func (m *myMatcher) AgeGreater(a int) *myMatcher { m.age = a return m } func (m *myMatcher) JSON(s []byte) ([]byte, []match.MatcherError) { var v struct { User string Age int Email string } err := json.Unmarshal(s, &v) if err != nil { return nil, []match.MatcherError{ { Reason: err, Matcher: "my matcher", Path: "", }, } } if v.Age < m.age { return nil, []match.MatcherError{ { Reason: fmt.Errorf("%d is >= from %d", m.age, v.Age), Matcher: "my matcher", Path: "age", }, } } // the second string is the formatted error message return []byte(`{"value":"blue"}`), nil } func TestMatchJSON(t *testing.T) { t.Run("should make a json object snapshot", func(t *testing.T) { m := map[string]any{ "mock-0": "value", "mock-1": 2, "mock-2": struct{ Msg string }{"Hello World"}, "mock-3": float32(10.4), } snaps.MatchJSON(t, m) }) t.Run("should create a prettyJSON snap", func(t *testing.T) { value := `{"user":"mock-user","age":10,"email":"mock@email.com"}` snaps.MatchJSON(t, value) snaps.MatchJSON(t, []byte(value)) }) t.Run("should marshal struct", func(t *testing.T) { type User struct { Name string `json:"name"` Email string `json:"email"` Keys []int `json:"keys"` } u := User{ Name: "mock-name", Email: "mock@email.com", Keys: []int{1, 2, 3, 4, 5}, } snaps.MatchJSON(t, u) }) t.Run("should create a prettyJSON snap following by config", func(t *testing.T) { value := `{"user":"mock-user","age":10,"email":"mock@email.com"}` s := snaps.WithConfig(snaps.JSON(snaps.JSONConfig{ Indent: " ", SortKeys: false, })) s.MatchJSON(t, value) s.MatchJSON(t, []byte(value)) }) } func TestMatchers(t *testing.T) { t.Run("Custom matcher", func(t *testing.T) { t.Run("struct marshalling", func(t *testing.T) { type User struct { Name string `json:"name"` Email string `json:"email"` Keys []int `json:"keys"` } u := User{ Name: "mock-user", Email: "mock-user@email.com", Keys: []int{1, 2, 3, 4, 5}, } snaps.MatchJSON(t, u, match.Custom("keys", func(val any) (any, error) { keys, ok := val.([]any) if !ok { return nil, fmt.Errorf("expected []any but got %T", val) } if len(keys) > 5 { return nil, fmt.Errorf("expected less than 5 keys") } return val, nil })) }) t.Run("JSON string validation", func(t *testing.T) { value := `{"user":"mock-user","age":2,"email":"mock@email.com"}` snaps.MatchJSON(t, value, match.Custom("age", func(val any) (any, error) { if valInt, ok := val.(float64); !ok || valInt >= 5 { return nil, fmt.Errorf("expecting number less than 5") } return "", nil })) }) }) t.Run("Any matcher", func(t *testing.T) { t.Run("should ignore fields", func(t *testing.T) { value := fmt.Sprintf( `{"user":"mock-user","age":10,"nested":{"now":["%s"]}}`, time.Now(), ) snaps.MatchJSON(t, value, match.Any("nested.now.0")) }) t.Run("http response", func(t *testing.T) { // mock server returning a json object s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { payload := fmt.Sprintf( `{"data":{"message":"hello world","createdAt":"%s"}}`, time.Now().UTC(), ) w.Write([]byte(payload)) })) res, err := http.Get(s.URL) if err != nil { t.Errorf("unexpected error %s", err) return } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Errorf("unexpected error %s", err) return } snaps.MatchJSON(t, body, match.Any("data.createdAt")) }) }) t.Run("my matcher", func(t *testing.T) { t.Run("should allow using your matcher", func(t *testing.T) { value := `{"user":"mock-user","age":10,"email":"mock@email.com"}` snaps.MatchJSON(t, value, Matcher().AgeGreater(5)) }) }) t.Run("Type matcher", func(t *testing.T) { t.Run("should create snapshot with type placeholder", func(t *testing.T) { snaps.MatchJSON(t, `{"data":10}`, match.Type[float64]("data")) snaps.MatchJSON( t, `{"metadata":{"timestamp":"1687108093142"}}`, match.Type[map[string]any]("metadata"), ) }) }) } go-snaps-0.5.16/examples/matchSnapshot_test.go000066400000000000000000000107321511036776100213640ustar00rootroot00000000000000package examples import ( "bytes" "flag" "fmt" "os" "testing" "github.com/gkampitakis/go-snaps/snaps" ) func TestMain(m *testing.M) { v := m.Run() snaps.Clean(m) os.Exit(v) } func TestMatchSnapshot(t *testing.T) { t.Run("should make an int snapshot", func(t *testing.T) { snaps.MatchSnapshot(t, 5) }) t.Run("should make a string snapshot", func(t *testing.T) { snaps.MatchSnapshot(t, "string snapshot") }) t.Run("should make a map snapshot", func(t *testing.T) { m := map[string]any{ "mock-0": "value", "mock-1": 2, "mock-2": func() {}, "mock-3": float32(10.4), } snaps.MatchSnapshot(t, m) }) t.Run("should make multiple entries in snapshot", func(t *testing.T) { snaps.MatchSnapshot(t, 5, 10, 20, 25) }) t.Run("should make create multiple snapshot", func(t *testing.T) { snaps.MatchSnapshot(t, 1000) snaps.MatchSnapshot(t, "another snapshot") snaps.MatchSnapshot(t, `{ "user": "gkampitakis", "id": 1234567, "data": [ ] }`) }) t.Run("nest", func(t *testing.T) { t.Run("more", func(t *testing.T) { t.Run("one more nested test", func(t *testing.T) { snaps.MatchSnapshot(t, "it's okay") }) }) }) t.Run(".*", func(t *testing.T) { snaps.MatchSnapshot(t, "ignore regex patterns on names") }) t.Run("withConfig", func(t *testing.T) { t.Run("should allow changing filename", func(t *testing.T) { snaps.WithConfig( snaps.Filename("custom_file"), ).MatchSnapshot(t, "snapshot data") }) t.Run("should allow changing dir", func(t *testing.T) { s := snaps.WithConfig(snaps.Dir("testdata")) s.MatchSnapshot(t, "snapshot with different dir name") s.MatchSnapshot(t, "another one", 1, 10) }) t.Run("should allow absolute path", func(t *testing.T) { dir, _ := os.Getwd() snaps.WithConfig(snaps.Dir(dir+"/absolute_path")). MatchSnapshot(t, "supporting absolute path") }) s := snaps.WithConfig(snaps.Dir("special_data"), snaps.Filename("different_name")) s.MatchSnapshot(t, "different data than the rest") snaps.MatchSnapshot(t, "this should use the default config") }) } func TestSimpleTable(t *testing.T) { type testCases struct { description string input any } for _, scenario := range []testCases{ { description: "string", input: "input", }, { description: "integer", input: 10, }, { description: "map", input: map[string]any{ "test": func() {}, }, }, { description: "buffer", input: bytes.NewBufferString("Buffer string"), }, } { t.Run(scenario.description, func(t *testing.T) { snaps.MatchSnapshot(t, scenario.input) }) } } // You can use -update flag to control if you want to update the snapshots // go test ./... -v -update var updateSnapshot = flag.Bool("update", false, "update snapshots flag") func TestUpdateWithFlag(t *testing.T) { snaps := snaps.WithConfig(snaps.Update(*updateSnapshot)) inputs := []string{ "lore ipsum dolor sit amet", "consectetur adipiscing elit", "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", "Ut enim ad minim veniam, quis nostrud laboris nisi ut aliquip ex ea commodo consequat.", } for i, input := range inputs { t.Run(fmt.Sprintf("test - %d", i), func(t *testing.T) { snaps.MatchSnapshot(t, input) }) } } func TestParallel(t *testing.T) { type testCases struct { description string input any } value := 10 tests := []testCases{ { description: "should snap an integer", input: 10, }, { description: "should snap a float", input: float64(10.5), }, { description: "should snap a struct", input: struct { user string email string age int }{ "gkampitakis", "mock@mail.com", 10, }, }, { description: "should snap a struct with fields", input: struct { _ struct{} name string id string }{ name: "mock-name", id: "123456", }, }, { description: "should snap an integer slice", input: []int{1, 2, 3, 4}, }, { description: "should snap a map", input: map[string]int{ "value-0": 0, "value-1": 1, "value-2": 2, "value-3": 3, }, }, { description: "should snap a buffer", input: bytes.NewBufferString("Buffer string"), }, { description: "should snap a pointer", input: &value, }, } for _, scenario := range tests { // capture range variable s := scenario t.Run(s.description, func(t *testing.T) { t.Parallel() snaps.MatchSnapshot(t, s.input) }) } } go-snaps-0.5.16/examples/matchStandaloneJSON_test.go000066400000000000000000000057501511036776100223530ustar00rootroot00000000000000package examples import ( "fmt" "testing" "time" "github.com/gkampitakis/go-snaps/match" "github.com/gkampitakis/go-snaps/snaps" ) func TestMatchStandaloneJSON(t *testing.T) { t.Run("should make a json object snapshot", func(t *testing.T) { m := map[string]any{ "mock-0": "value", "mock-1": 2, "mock-2": struct{ Msg string }{"Hello World"}, "mock-3": float32(10.4), } snaps.MatchStandaloneJSON(t, m) }) t.Run("should create a prettyJSON snap", func(t *testing.T) { value := `{"user":"mock-user","age":10,"email":"mock@email.com"}` snaps.MatchStandaloneJSON(t, value) snaps.MatchStandaloneJSON(t, []byte(value)) }) t.Run("should marshal struct", func(t *testing.T) { type User struct { Name string `json:"name"` Email string `json:"email"` Keys []int `json:"keys"` } u := User{ Name: "mock-name", Email: "mock@email.com", Keys: []int{1, 2, 3, 4, 5}, } snaps.MatchStandaloneJSON(t, u) }) t.Run("should create a prettyJSON snap following by config", func(t *testing.T) { value := `{"user":"mock-user","age":10,"email":"mock@email.com"}` s := snaps.WithConfig(snaps.JSON(snaps.JSONConfig{ Indent: " ", SortKeys: false, })) s.MatchStandaloneJSON(t, value) s.MatchStandaloneJSON(t, []byte(value)) }) t.Run("matchers", func(t *testing.T) { t.Run("Custom matcher", func(t *testing.T) { t.Run("struct marshalling", func(t *testing.T) { type User struct { Name string `json:"name"` Email string `json:"email"` Keys []int `json:"keys"` } u := User{ Name: "mock-user", Email: "mock-user@email.com", Keys: []int{1, 2, 3, 4, 5}, } snaps.MatchStandaloneJSON(t, u, match.Custom("keys", func(val any) (any, error) { keys, ok := val.([]any) if !ok { return nil, fmt.Errorf("expected []any but got %T", val) } if len(keys) > 5 { return nil, fmt.Errorf("expected less than 5 keys") } return val, nil })) }) t.Run("JSON string validation", func(t *testing.T) { value := `{"user":"mock-user","age":2,"email":"mock@email.com"}` snaps.MatchStandaloneJSON(t, value, match.Custom("age", func(val any) (any, error) { if valInt, ok := val.(float64); !ok || valInt >= 5 { return nil, fmt.Errorf("expecting number less than 5") } return "", nil })) }) }) t.Run("Any matcher", func(t *testing.T) { t.Run("should ignore fields", func(t *testing.T) { value := fmt.Sprintf( `{"user":"mock-user","age":10,"nested":{"now":["%s"]}}`, time.Now(), ) snaps.MatchStandaloneJSON(t, value, match.Any("nested.now.0")) }) }) t.Run("Type matcher", func(t *testing.T) { t.Run("should create snapshot with type placeholder", func(t *testing.T) { snaps.MatchStandaloneJSON(t, `{"data":10}`, match.Type[float64]("data")) snaps.MatchStandaloneJSON( t, `{"metadata":{"timestamp":"1687108093142"}}`, match.Type[map[string]any]("metadata"), ) }) }) }) } go-snaps-0.5.16/examples/matchStandaloneSnapshot_test.go000066400000000000000000000015041511036776100233720ustar00rootroot00000000000000package examples import ( "testing" "github.com/gkampitakis/go-snaps/snaps" ) func TestMatchStandaloneSnapshot(t *testing.T) { t.Run("should create html snapshots", func(t *testing.T) { snaps := snaps.WithConfig( snaps.Ext(".html"), ) snaps.MatchStandaloneSnapshot(t, `

My First Heading

My first paragraph.

`) snaps.MatchStandaloneSnapshot(t, "
Hello World
") }) t.Run("should create standalone snapshots with specified filename", func(t *testing.T) { snaps := snaps.WithConfig( snaps.Filename("my_standalone_snap"), ) snaps.MatchStandaloneSnapshot(t, "hello world-0") snaps.MatchStandaloneSnapshot(t, "hello world-1") snaps.MatchStandaloneSnapshot(t, "hello world-2") snaps.MatchStandaloneSnapshot(t, "hello world-3") }) } go-snaps-0.5.16/examples/matchStandaloneYAML_test.go000066400000000000000000000047371511036776100223500ustar00rootroot00000000000000package examples import ( "fmt" "testing" "time" "github.com/gkampitakis/go-snaps/match" "github.com/gkampitakis/go-snaps/snaps" ) func TestMatchStandaloneYAML(t *testing.T) { t.Run("should make a yaml object snapshot", func(t *testing.T) { m := map[string]any{ "mock-0": "value", "mock-1": 2, "mock-2": struct{ Msg string }{"Hello World"}, "mock-3": float32(10.4), } snaps.MatchStandaloneYAML(t, m) }) t.Run("should marshal struct", func(t *testing.T) { type User struct { Name string `yaml:"name"` Email string `yaml:"email"` Keys []int `yaml:"keys"` Tags []string `yaml:"tags"` } u := User{ Name: "mock-name", Email: "mock@email.com", Keys: []int{1, 2, 3, 4, 5}, } snaps.MatchStandaloneYAML(t, u) }) t.Run("matchers", func(t *testing.T) { t.Run("Custom matcher", func(t *testing.T) { t.Run("struct marshalling", func(t *testing.T) { type User struct { Name string `yaml:"name"` Email string `yaml:"email"` Keys []int `yaml:"keys"` } u := User{ Name: "mock-name", Email: "mock-user@email.com", Keys: []int{1, 2, 3, 4, 5}, } snaps.MatchStandaloneYAML(t, u, match.Custom("$.keys", func(val any) (any, error) { keys, ok := val.([]any) if !ok { return nil, fmt.Errorf("expected []any but got %T", val) } if len(keys) > 5 { return nil, fmt.Errorf("expected less than 5 keys") } return val, nil })) }) t.Run("YAML string validation", func(t *testing.T) { value := `user: mock-user age: 2 email: mock@email.com` snaps.MatchStandaloneYAML( t, value, match.Custom("$.age", func(val any) (any, error) { if valInt, ok := val.(uint64); !ok || valInt >= 5 { return nil, fmt.Errorf("expecting number less than 5") } return "", nil }), ) }) }) t.Run("Any matcher", func(t *testing.T) { t.Run("should ignore fields", func(t *testing.T) { value := fmt.Sprintf(`user: mock-user age: 10 nested: now: - "%s"`, time.Now()) snaps.MatchStandaloneYAML(t, value, match.Any("$.nested.now[0]")) }) }) t.Run("Type matcher", func(t *testing.T) { t.Run("should create snapshot with type placeholder", func(t *testing.T) { snaps.MatchStandaloneYAML(t, `data: 10`, match.Type[uint64]("$.data")) snaps.MatchStandaloneYAML( t, `metadata: timestamp: 1687108093142 `, match.Type[map[string]any]("$.metadata"), ) }) }) }) } go-snaps-0.5.16/examples/matchYAML_test.go000066400000000000000000000027331511036776100203310ustar00rootroot00000000000000package examples import ( "fmt" "testing" "time" "github.com/gkampitakis/go-snaps/match" "github.com/gkampitakis/go-snaps/snaps" ) func TestMatchYaml(t *testing.T) { t.Run("should match struct yaml", func(t *testing.T) { type User struct { Name string `yaml:"name"` Age int `yaml:"age"` Email string `yaml:"email"` Address string `yaml:"address"` Time time.Time `yaml:"time"` } snaps.MatchYAML(t, User{ Name: "John Doe", Age: 30, Email: "john.doe@example.com", Address: "123 Main St", Time: time.Now(), }, match.Any("$.time").Placeholder("mock-time"), match.Any("$.address").Placeholder("mock-address")) }) t.Run("custom matching logic", func(t *testing.T) { type User struct { Name string `json:"name"` Email string `json:"email"` Keys []int `json:"keys"` } u := User{ Name: "mock-user", Email: "mock-user@email.com", Keys: []int{1, 2, 3, 4, 5}, } snaps.MatchYAML(t, u, match.Custom("$.keys", func(val any) (any, error) { keys, ok := val.([]any) if !ok { return nil, fmt.Errorf("expected []any but got %T", val) } if len(keys) > 5 { return nil, fmt.Errorf("expected less than 5 keys") } return val, nil })) }) t.Run("type matcher", func(t *testing.T) { snaps.MatchYAML(t, "data: 10", match.Type[uint64]("$.data")) snaps.MatchYAML( t, "metadata:\n timestamp: 1687108093142", match.Type[map[string]any]("$.metadata"), ) }) } go-snaps-0.5.16/examples/special_data/000077500000000000000000000000001511036776100175705ustar00rootroot00000000000000go-snaps-0.5.16/examples/special_data/different_name.snap000077500000000000000000000001051511036776100234200ustar00rootroot00000000000000 [TestMatchSnapshot/withConfig - 1] different data than the rest --- go-snaps-0.5.16/examples/testdata/000077500000000000000000000000001511036776100167705ustar00rootroot00000000000000go-snaps-0.5.16/examples/testdata/matchSnapshot_test.snap000077500000000000000000000003001511036776100235220ustar00rootroot00000000000000 [TestMatchSnapshot/withConfig/should_allow_changing_dir - 1] snapshot with different dir name --- [TestMatchSnapshot/withConfig/should_allow_changing_dir - 2] another one int(1) int(10) --- go-snaps-0.5.16/go.mod000066400000000000000000000007401511036776100144500ustar00rootroot00000000000000module github.com/gkampitakis/go-snaps go 1.22 require ( github.com/gkampitakis/ciinfo v0.3.2 github.com/goccy/go-yaml v1.18.0 github.com/kr/pretty v0.3.1 github.com/maruel/natural v1.1.1 github.com/sergi/go-diff v1.4.0 github.com/tidwall/gjson v1.18.0 github.com/tidwall/pretty v1.2.1 github.com/tidwall/sjson v1.2.5 ) require ( github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/tidwall/match v1.1.1 // indirect ) go-snaps-0.5.16/go.sum000066400000000000000000000071661511036776100145060ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= go-snaps-0.5.16/images/000077500000000000000000000000001511036776100146065ustar00rootroot00000000000000go-snaps-0.5.16/images/logo.svg000066400000000000000000001627401511036776100163010ustar00rootroot00000000000000go-snaps-0.5.16/images/summary-obsolete.png000066400000000000000000001273671511036776100206430ustar00rootroot00000000000000‰PNG  IHDRAž ŲHsBIT|dˆtEXtSoftwaregnome-screenshotļæ>*tEXtCreation TimeFri 30 Sep 2022 22:18:55 BST$‡×Ē IDATxœģŻw˜TÕżĒń÷Ł^`é½³ J•"öŽ-Ę®±ÄcIŌ`‰±ÅKüÅ{I4öŠ(Ø((²Kļ½ī.Ūē÷ĒąĀ ź īš÷kŸy–½sī¹ßsīåŸĻsī½Ann^I’$I’$IŖe”]]€$I’$I’¤Ÿ'ĆGI’$I’$I aų(I’$I’$)!’}€&żžČ=—uŖž;”Q“~1³'æÉžųˆ¹›‚ķ³‚£øģ?'±Ėłšś_󏙩ŪōY§å”œü«żčž×œz²nć ęOĻŪ’y/V…ān'I’$I’$©ö%<|„hąøąćē?'L$#‡¶Ņ{ŲEü¾]ē^ó^¶™}{Š”l9kÖ5”ӀÖ0syļsżŠk’t0MJgņłØW˜S‘L˦¹äõ?š?~•/VÅ×N’$I’$IRbģ”š‘ Œåļ¾Į«SæYåų$C®üō;œóھÅCóRŖ›ę īHxöć¼¾čĪčÖx»FW{žr8-™Ģ#WÜĪ{…[V/†SŚŃ4#w;I’$I’$I‰±SøHæfN\JPYĘM·ÜvVŌ‡]’X6y2_~:‹MĶūqDZeżZ5M%“dr@ ²l.‹×q·“$I’$I’”»l `ŻÖ9”QT±„„“®}É­»œ9_-eƔ/˜ÜŽī}“kģ·ŗŖš÷bßz•ßīr‡ŚI’$I’$IJŒ>&×oLĆ iŲ“#żö¹œsöo@dć|üՖUˆ-v”~ńL&ĶMfSÅ[Ģ›F»~=jōóÕK“Y—Śƒsīł#.?Ć÷ėAnš6Ē‹µ$I’$I’¤Ä7hŠ`D"Õr‡ōkNÓ>qš!sČAĆčß§¾ąÅūäŻe[nĒŽ’—§ÓqÉ+<ųį"DØjyĆś…Yōā$ѐ²hÅx&]IČķ¾'½{bŸ#ö§g‹ Œłd~u_±¶“$I’$I’”;ķm×sF>țła(/bŻź%LŸ}Żtdó³ 32d6–¾²˜ P4g e‡ģÉŽK™83­ŗż²éÆp’ˆWh×uv$ĆūŸĻ=‹øģļsāj'I’$I’$)1vŚŪ®Wö1㷺Śo½ó%«×Žt¬ŠĄį·ņ·Ć¾iŖ Ų£_G˜¹Øŗmd«—×Ģ™:Š9SG±nÄæłEÆaō,Åē©”øŚI’$I’$IŖ};'|ˆD ųī·Lēõi …yś±XYmTeŃó„óÖ­°hū;nīržŅb"³ÉJŽ~³˜ŪI’$I’$IŖ»ÅŅæ“²ōčšDɓѼńѧLüd?™Ä'“ĘšĮ§«(oߕĆr*h±ēžŪģŸڃ½»d^±‚±A(®v’$I’$I’cē­|ü©ŻöfĻģRMÉßęvģ„Sf±éųīģŻ+“7F—’wšļ¹õ¹Ģųj:³ēƦ°n3zōL×f(ųēÕūĒŚN’$I’$IRbģįcó]Č /f֔@Ķū”˾ų’y„żč4° Œžœé/?Ķ»GōeÆ=‡78ƒ”ņbV/›ĮØ{Ÿå±qŖ÷‹µ$I’$I’¤Ärsó"?ÜL’$I’$I’āćƒ%I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’ĀšQ’$I’$IRB>J’$I’$IJˆ¤]]€“³¤¤ōį„+OgXnC2S#łrņˆvuY’$I’$I?[†’ƒ’C]8č7§sXצ¤§±jśxžūĒL\Ž-śK”Žē]Ča¹K÷Ÿē™¼¾6ĢLų1³:Ź”6šÜSµrÖv»ŹĻe’$I’$éūyŪõ’ Ææ†Sz§±`āó¼ōÖ"ķē²[Τ[IXCιæ?ń’ćO\yhŪZIģR×w”K×t6M’7¼=– &0įė5 ?nĪ^ūpÄ1CŲ7¹j·ģoWł¹ŒC’$I’$}?ĆĒ’1 ņ.įČ.UĢż÷ĶüłŸošŹ3įī{'SX8'œž:y7g÷ZĘ÷ŽĘ=O.¢īńWqQ·:įL’R ¢øh§·śų;–ėī“žv•ŸĖ8$I’$IŅöķ“Ū®ėķq§ž4”.ķA9ēOcĀ«ņ̤uÕm2ź㤠 OnsźQČŹ…ŸóĪc’āõY„Ńļ7 ć‚’žOū±ąā‡ęŌčæÅ~·sūÆĖyįÕ<¹"€pz<ķdī׊FY°~IŸ?’MXYcߌMĆøųµóiöņÕÜ?ė0N:­7¹M’Ö®äóĒoąž ›§wဓޤĻ6“¬S—Ģ h›ś¾ŃØÕIœqÉ`:·L§bÉt>|l2uÆ<‹NŻĄ„o©;ÖśjSĖa=ɬĢēÅ1+€čmŃk'¼Ļg…½8 '¼5¶ÖūkÕµ!‹ßø–7>-fīń§īÕ¦.ūQcł”ė Ń›ųėłķ…BAŻĢSż£ĮgPš§Žßxæ}Üś”Røa1łorū}ćH-éĪÆžøŠ”U„Ba‚²VœóijœU=īĀ7.įŚg£×},×U<ż}Óē÷]WYĮQüī¹Ć‰¼ó%Éż÷¦eÉTž¹ū#šŸuū“.aŃ[åŗgfÅÜ@ć”·p÷o`äMļ|Äń ĢmHRÕb¦ž÷īzsŽć‡ęY’$I’$ķžvJųŲ¤ėo¹žŗ^ŌYśc’;й)4ĖĄ÷ā™IČ97ŸG挙|4r³6“ ßC9iDC’~{//O¢8ż}>Ÿv}zj†Żµ!“ųæÕĮcrø §Üx56ZÄ'£žĶėĖRiÓk]öGźD®ä®K¶©3£Ūł\1°˜/Žüļ¬,'§Ķ0öh l"£ī0Ś€“'2zöV†[ŠmČ~œ4¢I×^ĖĖ £S™‘y$Ür$ ?gĢ““Y™Ó—ƒ~}éi!J·Zgo}kqęėÓ?cϟvÆnڱē)¶lAhż"FoŁæ$kKę$7kG·ņ™šŌj+n Ißįä½7Šåu†1`Ļ*V?ŗ¢ŗ}J½Ž4M*`ĮŹŲĖõ°ž‹GłÓuČ*źĘ~7NnžSÜūŚbŹ7Ī‹łxÕµ–wį”ĪgxĘƼų&ó ShŌŗ śwgHd,cƒ„i_ņś=wņIr„ĘĪįWūÆgō­ĻńYeA9›–V÷ĖuO±^WAUķrKxž™ ōūÕ>œžĒęäzŽ—–Ή‡ĄA/ßĀČāp\×iØ¢%CĻŻ—IĻż;žFĒĆĪęŌ_]ŹÆ¦_ÄćsSāG,ó,I’$I’v S7ķÅaõ¦Įņ×¹õŖg˜ZńMĄōOmÕ®ÅaĒÓ·ń Ę^#’˜m3žƒÕ\ūĄ/8čä.¼|O>ÓĒ̦ä·=9½żĆ<1'€Œš!ōéał+[VDµ>ōh±·Æł#O-Ü|‹ļčWYųĒĒ9ż¤Ćąć¶©5«M’¾ņ&^_ŗyZ&}Ę»›æ+\ö —œY³żų·WsÉæNcæƒŪńņ?Šń„Céœ4“o¾—%£™¶ņ6žxv[ƏܑśjCĆlŠVŠŖ²?‚c›Œę+^`]Q9U9õØޱ‡€±ō7į…‡}żÅŒxä4"”B~ōLmB÷”ū³ļ¾½h[w _<św[ū-ѱ^/åē1ćKH+L”W9T¬›ĮW_Īkζ–ܼ]š—RšĄm<üĮ7÷ æ Aˆn[:ķK–-Ś Š˜÷՗|U±m`ėukń\WsF=ĀėļEXßc ēwł‚—ɜF é7tm[UĄĢpÜ×éŠŃwóϱŃqńß^cÆ>gҽk˜»,®qÄ:Ļ’$I’$i÷–šåCI{ ¢gƒ2f¾ń"S+"ßśłFĒžĶ ­śœĢŲҦxć+|4+ Nē^Õķ6L˜@ž¦¦tŅŖz[Ę ½iZÄäēWoė9ø9I Žē©…U5Ž—?n)‘ʝ80¼ķ3+g¼ĮėK“¶[ć··Eˆ@į\–® ØŪ“Qu{ōĢ!¼`/.ŪŅĻŹŃXŖčÅ[ߦz晳ég|6q: «b·–ŗ¾ I)(/§Ŗ°%ķ󟐯“-2"T–@Uj2)qd:±öW¹éKžżż¹œwå­<šĻ7ÉĻ<šßŽzūv^Īų'žĄåWÜÅćSŠćK¬×˶WZĶsÆŠŗJ #©ŌļŁuū×ķޱż£nŪ«Xū‹õŗŠ„ Y涜6m¬ “¦ˆ)É‘k)Š$S7+)®žŖĀ+™żéźź6•«W°²ršÄ?ŽXēY’$I’$ķŽ¾ņ1­IcźEL]°ž©ßŁ®^Ö-Ś&˜Yµŗ:4”o$ĀÄ `S0ЉÓOćܞƒįńčŖ«n;’¾ų5žZ˜ DH+ģMƒ†"gņē§×8N(œD$„œ¬†U°¼föŗqĮņļ 6ö8ų"N·.TExetfZaogE(ž[³ŸŖÕ+YWõ晓Ø/B„÷žöŽ¢ĻVŒ?€)«;Š²’“YR÷Sžŗńv>NŸĆWÅU NƒPi9eq䚱ö—”ŗ'œuC»7¢“ąSʎ¼—ŃEūqŚÅ'pa惙7įF<üi\c‰õz©m…åĻšÖż9š5<łčjęϚĶü_2ī­±L[U¹C}žŠu«˜Æ«oīx/‰ĪO@PøÕ\&CŹü? JYæa«k6©<ś;žD{³D̳$I’$IŚłvŚ g~¬Ńہ#D˜6fÅæėĮ)žå„凊d–½?v«š$€ż4wæ4ŪĪŹ‹łzYŅ6w—•—ēńŪqWžšĆŖ_å©/bEQ9įņĘ ½š|†lUe¬£Ł‘ś~슯Vm„HƒĘDˆP4’ ¾ '3™Šŗµ¬« ĘĢ+ÖžŖ*7±zŹ‹\ūą 2*÷ēāĒ~÷aīÖ”Īø˜?y9×¾ŗöGokß\/µ-B„ \ĀĢ‘ƒéßgO:ļ±Ż›}÷ęį+īąż¢ųžÅŪu«Ų®«¬ļ¹L#įrŹųfęvģ:­ µ=Ļ’$I’$i×HxųX²|%k#ķh޲Ģ,żĪvkWC¤Y‹m¶7h ›VšńVĮŽĘ±“˜uį©tŠ”Ń śŅ1sŽ]Ą7©YIę$VÆČČ,åė©_o’€q&żµ&łėæqõŖ·„oŹ`@vVQ}ܕ…éM×Ų7Tæ!9[z‰Ø/V‹SÕ¾%dT2jóKbŅ {Ó¼M„ņÅs™ĒĖfb€OĘR=¦“Ž}č¼öuĪ|ś¤õä®ćūĀ«#c>n<×K­ `ͬxsÖG¼ 4ļ~ų}/†õĖążŃŪæĘS"ŪŽc¹®bķÆ¶Æ«q~×¼|Ów¼ó,I’$I’v/ ęcŌ L[“Jė#Ž!7ōŻAĆ쯖iŲ“sņ¶“IĻ:‚Į#lĢ’¼FČQ”ō“gWŠzЁģ;¬3«>ē_ ¶J÷ų쳄Tv8sņ’£ūnõÉn\'īq¤¤‘H~źܟ^™[)€i3ÖQÕf‡åTTo®;Ø­«j¶Ū‘śŗģJ~{ÅńōHŻöy•±Z:fÅį\† ݐęōŻ—^Y„,śä‹ķ†Ißwܘśūęó `Ė\›"ē-Ņń\/ ±Õø6ĪYĮĘ 9mŪė»lu9A$“Œ†Ūæöcŗ®bķÆ¶Æūü?ŠißŖ!–y–$I’$I»§„Æ|,Mūœ—üœ®×ĮļīhĢc¾biaM»õ§_ö;\zŪG,zżE&x!Ć/恊«˜UՂž£]x&/żūk¢Ļ9Ü,€ÆĒ,¤ō×qpćdÖ~šŃ6aӒēŸä½½/c’ėn£Ž˜Q|5·’póÖtl·=2ßä¬?ľŹąĖ/×sЁgpĆy9Œ-(%³y?†jBéƚžżŌŪä8‘Æ»‚ģ×>cEƒ½9xhŹd}ékŅ”W/ŗgdPš`„/āŖ~‹wšŹœ'8åä?pUżw˜¶©CīEöš‘<šÖ¾=‘?tÜxū(žž)3ėÅՇ/åõ‚¦ ?³Ė?}(®qÄu½Ō¢:mĪåĘ+[°dŅT¾\ø”Ź -½ŽF‹ŹÆyśćMŪ·hĘlVVīĻžē’‚u#g³®<`ӂƘ¾ł¹…±^W±öWŪ×}m÷ė8āgI’$I’“{ 7hŠ`D¢²qł8¾˜”A^/śõLæžķ©—¼ˆIoŒbŹā*Ŗ0õ“µ¤wŽ‹~ƒÓoĻf„WĮk÷ŽĒĖó¶}ĮDŒtö8®SVšéæžäÓµ5ƈŹČR¾ž`E :°gÆ! ܍MsÆŸĪ˜‘ļ3cQQuŪ䊶ō=„Ł3F2rJįvǰģó×YŪ“?½zīĆą^y4ĶZĄč¼ĄŖ=†Ń©ōcžūį"ŹĖf0óótZīŻ—~ūö¦K½µ|ö1¬܋† ßęĶĻ6Ä]@JIkzžŚV)«™öāfVģų¢ÕyPŚ¢ ½ aļ=źP4w Żõo>+Śvec,Ē§?€ŠŠŁĢŻ‚~§œĄŃĆŪ™ś$×<<-®1Ä{½$•µ Ē h¶l4ÆOÜńgKVF’ØÓ¾¹{u§ļĄģŻ­Ik>įõæżƒ7m{Üņ¢/X\܁ܞƒŁwŸĮ 8¶U£yoJōŗõŗŠµæX®«” 3ƒNČ£äƒWłxE@ÓŽG1øĮ×¼šĪ,’#čū‹½&¼ČøE”˜ÆÓĢ6Ć9ØĢxõ]¦—FƑ¤’ĘģuüPZ­Ė«ćVÄ9ŽųęY’$I’$ķž‚ÜÜ<ļaL°ģ¢ųõgŃą• ¹śéŚ{©Š$I’$I’“;Kų3é{u§}°†ESVļźR$I’$I’¤&įĻ|ü_“VŌ›S8˜”/?fʬ5„2öbŲɽHŸż÷}eÖ+I’$I’¤’†µ¬$ó3ę.<šCHßC26­bį“g¹ū”7wui’$I’$IŅNå3%I’$I’$%„÷K’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’ĀšQ’$I’$IRB>J’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’ĀšQ’$I’$IRB>J’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’Āš1Fū\ū.&Œgü‹ēģźR$I’$I’¤Ÿ„¤qC®ąās‡²g›fd‡6°zé<¦~ń/=ó"ęUīŒ~“  …ˆA­öŪūš+š[EhÖ+üåµ¹?ŖÆŸĆµ”üīå—×¾VŻ.Ülē]š ģՆęõ•^Bį’y|=īQ.½óżźv'żėc~³G%ŻÉĶóą¼£ŗŠ m# ?zŒ»n})E[nŸī0ä .9gŗµm@JŲøf 3ę~Ą37=Ąøuįu¤pōį7sŌyƒé\§”™ćļćŒßæQ£MN‡ćøųņ°W[X:ė3^~čn›]}˜y:w¾€®•BįčķŠ)}nfüų£,y„Ē’+!ó|Ģ_'qĶĄJ‚üŪčūĖ—kl }u{Ÿū& ßĖ‹w÷eŃČgXŽõxśdĢeä=O°ńø+9®sk>żĒ\žzĢķā9oCnĶ]fĄ’'¹éŽl;oŗvČ"(\§9‡»×]Ļwõ%%¼„·N=ž?DĻevé±Üųń•ōOްčé9ńÅ1Ļ”$I’$I’j_B_8ST’6,Cöæ€_ōŖóƒū ä¢ó{Š8”lr‡’–GOh\ż}N‡cųÅA=éŌ¼>Ł” l,ģfčܟyéŚ=ŖŪ…BaĀį0©.ä÷gt£iZ:™ié¼ßļøćżŖŪ5Ͻ?’åtŽö·Ŗ ²š·£Ē¾żéSÆ|›śBYGńėė÷§K“tBé9tŽļw<²U} š]ÄŻžŽĆ÷nCĆŌčšuĘłw>ȈĮ›¢²,Zߖ4 Žn«ØŒļ“ģČ<Ē¢ć'3ØE ©õ:qĈ[8eÆz¤„dÓdč™ÜøWU\ķb=o54<ŠßÜv½Ūg””’šŁœf “X’ś>+N"9ҊžGvØnž9|½ÓC6ŸqoĶ®•9$I’$IŅŽKųۮǼ0’h2œĖžž Æ3ŗā°Ķ‘h `ńSōr ‡0ŒžżpÕ½£™Y¼ķĀŠŹ˜ūōo8ēä{·*Z_īVõķ3ā$ŗ¦ėĒńĖO䗧ŽÅČE!BÉ-8ųŠ3(,|œ_éO’~ø’‹čŹŹ²Ļ®§æōļ7€}~ńpĀē9&óå¬A÷ņqy@UņRŽ8’ ®ULø¢Ķ:¤ĒÕ.Žó@J6æ‹ß2”}†汓Fšō¬ ė½Ę;Šh9hxuónu%-įü÷¹wNś¶żI’$I’$i§Jxų8õ…3¹lÄŪLYUN(H§i§ż8éź’ć½’ÜĄm*¶-hŁ8^ŸX¦ŌILZ@JŻ-+łJ ^ćė’^œ4ā~yę)ž{ī9n9&śTĆŹF-h‘]ó­Īį‚·¹ęĆB"D˜ųЇ,A(©3½{FWäU”GW7V5ĄĶ'  k½0"ŒōIF.Oަ¾Č†÷yś¾‰Ló,£æU_vé± éš@ž³wščų…Ģz’ś ėØj;ˆ+Ū”Łśē›Ģ-RckĀē9e«ē°xŁV–B$¼’y37²vE!™­²ćjļyØ`_õ<ה!Āŗ¹#ycÜF"D˜śźē¬ ²Ż .nWJęŖ”ō=ÖģńoķŠJ’$I’$©v%<|Œį‹7oäÜĆöį’žfŌWė( …Čj{׿įųmw(ŪĄÄŖhX*ŪÜɖšqŲÆrßu'rpŸnt銁ÖmZŃ¢q4š ")d×­ŖŁŻźiÕATŁŌy, ‚Ŗ:4h]Õ8ēæļ2­, īĄžæ½‹‡ŽÅŪĻžß“·Ż+Ų°’"!"DX_V³¾“Vķh™”*¼Šy_/®7MÉga( TوęyĮ6}ֆøē9Ai ‘ä2ŹK"At¬„e„¤V„ĘÕ.Žó“d/—‡«ĻĆÖĮ솷_gÜŖIe{0pH+Ņ÷9ˆ~uŖØH™ĮÄ× vh¼’$I’$IŖ] aKh4ńķūøīœCøåŻu@@ŅžĆ9.kŪUyßµj-;ć—hcBA@yž3\sĘaōė۟Ć7)zĖq$u;{„U’«*g뿢V/¾—+N’#O¼ö ³SJ§^»¾sĶßųū‘9ß9žŻQ¼ó¼³ģŲyƒeß9×EõĘšž„5t~$źE“$åēžŁß>Ė’$I’$IŚ>vīÖtĖAō3㣄DŅSÉIŻvÅŪwImŁžÖ›ļSžńŹŒ™¶hŲ¾>iߑ&·ķFŸ zŒŌN]iU!ŚĄš•Õ5­ó6Ü|)§»?Ēū{^˜YAˆŗ> ®±–,œĒāŅ€PeCZwn^½=­kGZUEWD./HLpė<——D›DR¶~š&6LŻ‘óöƒ˜öÜ'¬ ²ėQœP}ę¾õ $fq©$I’$I’ā”Šš1}CŽøćYžæļN;toŗtčH·^GsÖiŃ7—0oå¶/uł.Ė×°>ˆ&KMņ:A©)C8śšŽß½S££¹ōźaōéz(ēü~#PU™Ļē_Dūi9čR~÷‹Ձݚ…£™ŗ"śČō ¾kcŹ ŒŻ·ÓiWņ«ī-h×įPν¢'u#,Ļķsk®ŹŪX½=9¹m7¦mūÜĆXÄ3Ļ…7PŁz8ķŁŽ~ūż™ż;$6|Ü”óƒ_¾ĪĒ C„ŖŃ0'BEŹLŽ}wam”,I’$I’¤Z{ņ·CB„Č¤ÕŽGqQŸ#6æ\%   JYņźÓ¼Š=’\·ž>ޘ~"—uJ¦ŃQšR…TÖĶ„eĪw‡gįE49ę6ī; ‚ z¬ ļ?É6$AYöęŲĖO䈋+(ŻøŠÉ iV'•HPŹōńŸÅ7Ü>øńyŽ|ö:×ĀāüĶć­LZĀ»·?¾ĶŖ¼ü/@’ö Nä®÷ާ¢²’’yqąéĘqąŲēyśøĻYō ź†öā“<Å)įBJ×ućj˜ĪĀeėØ,®,bķģɼż·Ė8žĪYq÷łā#ųĻø…¬)ˤyóęT.x™;īž}vąv”å’ŗū3ęUT¬b֘æpķ-WTKg½É›ŸĢdia9iY-hžE+¦2ö™«8żŃåq×·jŃßøüā{ycŅ ŠŹ“W±tīūüē’ ¹įƒŪ“Ÿõōmüyät–VNJ"55•:ŁńeĀńĢóņ~Ė]ĻMgUiˆņ’åLyįv^ž:ńžŒ÷¼Å$€ł’ž…›ĖŸłÖƒGI’$I’¤ŻH››·{¾=åG:åŃI\Ś„’ņqW2ųŠvu9JFy#ųūÓÓ¢b œp/JŽÕ%I’$I’$i³ņ¶k)Qųõ`ZUAå×oJ’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’ĀšQ’$I’$IRB>J’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’ĀšQ’$I’$IRB>J’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%DŅ®.@;WĮÕæ„ż“aīrĻ›¾«ĖŃvœ{œ˜ +§{ʤ]]Ž$I’$IŅūɅUZ1ū”Nн1“NnüšuroZ¾CķžēĮ–O-*8loh™3É}oc­öż?'AēH’$I’$igūɅĶ;Ąmj­jÉ= [L^†’$I’$Iā§ųĢNJR˜±^ś–~ĻŹ°XŪI’$I’$IJˆ]¶ņ±ą˜ž>‡ÜgVĵ_Ź×Ƚxs¹=”YäGµ‹ĒŚž}YuF;h•A)¬-„3ɽy˳ ®9}ó3æ„wźĮ©Ķ!„>ž„Ü›ęV·«jԊŁgtīõ”^¤•Ć²ÕšĮ—ä>“hK÷Ÿ ¹UšÉXXŌj )ešÉ'äŽ2/īś¶HeĪĮĆ©<½%d—n·æ²vyĢæx/č\7Śßœ…d=ņ1Ķ&—ŹŁ“™’ém®Šžīu#7Ļõ¼É=˱ć«ļūÅ:/±Īs<õÅŚ®øS‹ĻŪjžV,‡’L"÷­u5ĒrŽAŃśS‹įĶqžń×Ŗ$I’$IŅī`—¬|,8ö8· õ¾\¹+æCŹ÷ĢŖ›÷„Ž›§²04iūęRŌz;aQ‹®p^³čæS2aŸ!œ^żuQ§=ąĄ–Š4#Ś_iMšĀ RpQ½-ż„BĮ^żį„&–éY0l8W7Łńśw¦ņ·ķ qņ꾆Pp|Zõ×U­»3’¾Įн.¤)©Š¹#…·ʬ~ŃĢ:Ŗ„ŠĶõU ¢‡Cڲā4īś~HŒóė<ĒZ_¬ķ*:ģĶāæ|kžZ¶†ßJĮÉÕķ N?Nh uS -Ž=„Ę?’$I’$I»”¾ņ±¬Ė 8;ī•F_ĆOe×¼į­ –L&ļō/ØŹŖĖ¬ƒ[µ DäŪ#IŪ@ųŖWčše}ņ’ļhh“ }Āó H]<īžBŽ[ĖŖwÉ?ė8„ Ė…&Öģ/»n|‰¼+Éq Nƒa]įöå;V_Vy–Ÿ5dэ A ģŻž­oöÅ=!-€µóÉøżc¬kŹĀk‡A›śD.ģ Ÿ| k¦“wXtµ_ž=ēDŸłųł(ņ~·°ś0ß5īśbõóė<ĒZ_¬ķę^Ņ%:‹§ŅlħdĻ­"’7ĒƑuाšģū$EZĮ” ˆĄŠ9Ō½ėkÖwģ ē5ß±¹$I’$IŚĶģŌ•””–Ģæ”#Œ{Ÿ¼77ķxą“+”ožŻØłĒ5#B„Pįzņž;mūćX¾€ŸF |5ĢÆŒn«æeåcźÜ9jIž-‘’ĻcČüh8póJ¼9Ūö7ēkņ>¬$B„öO͉nKi̬¾U;Vߌņž^AƌiPP³¾¤pt G·żw-?-"}Öl=¶ ŗ­U{ÖēFŲś§Zä;¶Ē[_¬~`^bžēXė‹”]8£ tK‚ žś„ģ¹UDˆŠł‘iŃ}³ZP0*Z7ƒ†‘h»§?¤Éä•ä=÷6|]µćó!I’$I’“Ł©+g^3ʾ"ļÖå?­ąh’Ī×Ģ9joHmLĮye°pIī~’¢ŃM[Ż®|kŽ’·}‘“m3挬/Vß÷ĢK<ók}q£¼ J*¢Ÿā2(,~$I’$I’ž$~åce]ū²č¦<óõfžˆēūķJ¤,XHīķŃąŖ¢ikęŽ8:¦ŽFĪ‹¹«²f9[Vį½1™Ģ9ŃžW6ŹžīZ5¢Ē.īŠxsME°¦ ×j}IKÖA)šIy‡T˜½×x}ĒoŽ[L£eŪ,#ü~µX_ ß3/qĶs¬õÅŠ.uéꕒA Üń ¹c¾ćzo]żÉfu‡$źēo^!Ł:}ūķ%I’$I’~b¾ņ±ż[ó!’ó?!÷ÉĀ<–7n̲>-XŁ»doī«~C–õiĮ’>Mćn«ÅCś0ėøĘŃ0+€¤e `õę°(-¾1„¬.ÜņG§zŃ;‚SZĮQõ¾{§Ę˜uAsŠņڱą7m¢ŪŹW’;!\ėõUTäĆōĶūž0”e=2(iŪŽågm~Ź¢¹äĢüÖ„SXżŻ¤ńvū¬Ķśjųžy‰gžc­/–vUk¦@A½½śŒ¬ķ’ ”5©Ļ¼}öb֝ż¢/_øVŃŪ³OėĻŗ=ź0ļĄż Ē.y ½$I’$IR­KųŹĒpĮZžOś¼ÕŪyę^üę  l~QČ7·&wķĒĘ[Ź™uŪtÄÜ.VÅMZĄy{1ėģ (Ś‘ Øæł„"ć—Ē5†Čņ/`Zč†CgV§DŌ‡œļ įVGˆw0KŽB›ėžšuõœÖf}ŠśŸ_°ąī^Pæ ooĶF‚čqƒ š_o{.ēl€ž9м;³ŽčŻ6o"/šQūõmķ{ę%žy޵¾˜ŚŠźļÓXxŪŠ¢3«īéÄźŠJ"Iįč-ąIóØd Œ\ §ä@“ά¼§°¶ź§īųœH’$I’$ķ&æÄ*€ōłµóQ’$I’$IRBøņQ’$I’$IRB>źĘÉcŅ›_ĢØŪņwu)’$I’$I’ &Z>˜ĆŲüb&¾“O×ņŸ’ō[·gilć BU„‚÷#eßŪ'ęÓ/ØŚÕåH’$I’$ķ°¤]]@¼Śü"S/£G»2ź×)#\ZĪāÉ’żs:/ĻHŽÕåķ2A@4X«å89ļōtn_BŚ”bīx!½v;’5ŽŻĮ7c B°›ē¤’$I’$Ißk§…ÉƒR8ó¬}÷ŲDe¬+1ī©ŽJ‰«Ÿ…9“é†-RĆ“W=·ˆ:Ē·ą‰üųśÓ÷kyB2'vZIņGKøć…¼]]ĪņĢ>ŶgŠs™ÜŚc9AårŽ<µ·MĪ`ŻĄ:¼śŲJŚP^Ż®ō…„xmn}c=ńœ·ōŽi\øÕ<ÆYTÅē’LęśgÖ8v÷?§sõ”i”²ž…Ļü-R7ī9“$I’$IŚ%|åć%“C)K*åųsėóā«!ĘĢJ¢ćeDV&±‰$źžTÅŁŻ"ģ’\&£^›ĆŒų^²Ń akĘ__é¾uø’޵ i·ŠXQžNz£€½öYEßśåŪ“/kՂ+ĻXA³H%U$ÓęąLžpņŠźļ—öNēŌż×Ó¹a19‘r6Bz£$zż2‰ūƛSŻ.„Čøż”4M/#=œD»CÓ¹ėę‚®Æ¢IS~sõjöĢ)$N¦Ż”i\·U}Ž3yąŁMŻ} )§*Ѱk˜³^Ć-ūFĆÅH”‚$ĀŌ|ę`xó§²bĖöxė«ķóIh{wFōXI2wIcnŸœ¹ĶXŖ?”mWŚĘz>bmW90“»¾5Ļ9­’vSOœ½ŗŗ]ņˆŗÜyÜZ§—N%÷ä$n¾I’$I’¤Ÿƒ„†‡ŌahÖ’×/ēį;WoĻīŹ ś…¤7*#*ŖB9õ™žŲc9™’¼Ē¦˜:».‡g®VóŃsĶć®±łQ!ŚQFŚ‚%\Ü„.ĒtKaH^6—ß—Ēō¢m†V«ųģĢ*閬æÖ6$Bˆö‡¬Æž¾Ł¬M/­©G$Čā ß/”G!»`Wę…’—Ķ ¢sš4n Cņ²’—ĶąĆ:1-9“CõÕöłhxAī=|é¬cņÕéüžŻ:DˆĪsżq…œ—Đ¼l\Śx»ūoķ‡ĪG¬ķ†čNĮ‚UÜzL*Cņ²¹šķ¦”’JĒKWsLv%æ>rT’²b’8/ƒ+īkNE݊mź’$I’$Iś)JģŹĒĘ!šRAŅ l¹]6ņļõ\š§Ę,Ūź®ļ”“J¬aäÆėóēĻ2·×Ū6J†eń«×R2VžF|–w‰å„Ń5|e­Ó8ńŌ2ö¬_I„ŸŽį­eŪ¾=;eE)ÆLČ"B„¦Eßžœ’³åūģOĖų8”ÅŃ÷…yhdĻ|P³G­‹+ZÖ©¬Ń_zA!W¾W—F?‰>æ2ČaļŽE;T_ņśu`+zg²gVPÉō{3xźż0ŸŽWÄŻāž7I’$I’¤ŻQbĆĒ•°Ž0ŁfÅßźĒ7rįVdˆ5Œ¼Ø>7½[€XŚ-æÜ_̳iĢ Ž½¾mŒńRMĖž[ŹWdPE}ż”’ū?.āķ·×rĶI…Ūķ/T6d¶ó}»fńĻėVqHĻBöh·‰VĶŹiYwKp•]·ę-åĖ©>N½ ™CJĆʛvؾšzXU!ŠҚßõΦ-„@1ó§dWGr §–3$ Ķsc_uŗ#õÕvUAˆtŖ€0»®ß”cnķ‡ĪG,ķÖä&сr ….wd0&æ÷ó‹“_ČJ€4ŁDeŻ$źQ ¬cęŌ¬źóńé¬ųŸ“)I’$I’“;Jhų˜÷ęz&UdR^·!gž¾¦Ęw"lxæė’Ԉ©s¼vQn•sx4ā”*†¦l xüR.æøć‡N‹¹ōÄ4{³ł«2)#Lvū»©Œ‡‹ļŁ{…28Ÿu„ˆžŗ’Ē„187‹&7¶¤$†©.n•NŚ·ĘQ›õ5žąX;j³¾믌łg°Œ$ŅMįŗ…?~P›mļ|ÄÖnĖ<—”¤,\ż)*K¢°, 4Hų»ž$I’$I’v¹„†"üå™:”FīõUüėʵōėYE‹®!†^ĮĆļlbD«~}p·ŹŠ9@k³¾é/eÅ*žrV˜ū¦6źsšĶKčŽļ…E5’Cē#†vŁ +XF(cŚ„eÜ5ķ[ŸLĪ|¼1i++YKČ”uĒ’ź¾÷n¹ćõK’$I’$ķNž¶ėĀ›×ń›1M(%“¼“Søė?%<óR!·œ½Š¶A9õŚŅ)\sšxÜ ™œŻf)ėWņŅŪ“¹_˜ < `h÷Źīą[RŽLēź3Ji[§ Ų4½” +SH3ÓL[Yµ9L Ó¤Kiõø‹ųī[™Ėš4äņėčŽ?™æ^UBS*±ŽĻ&gŌz}šFAō¶öŽēqFŸršõLįžK‹É”’”ޜŸZc—µe›wĶ…ĮéŪĪo­Ö·ż…J RTÅĖ ˜I A§F\uĢŚų¼ŁXŚeå1fA] …ī——plær NĒŻOHįŖgĖ8&„‚z_ņuQ&¦×•k9nļ*ڟĶ„{®Üįś%I’$I’v'‰æ÷3€iēmäĢćpīetė\L嬛SÉä'Søå©ŽÕķbŃwĻ D(ÆŪˆćo-ćXŹŖæĖųr{ŸWyÅ­Ó8ģ7•ö‡rŠ×ĆŚ“dš§.*™>¦n\}՛RČ3 rø¹õr²NiĄ‡l¢('vŁ+ų®œ7y]*uN qß)ė 6°Åo…xf}2µ[Ą{7V1å© öĢiŹŁĻ”p&儈żĒf|^Y³ĪĻ>ĮžPޤ·~¹‰²H@hŚ.:¬#ÓS‚ZÆoGū«7v#·oČ#Ėčņ»%2Ŗ.o­OāÄwŅ8ÆMtÅdJ}öfŚ1-x÷č" ŠĻÆä˜ėŚW÷óCē#ÖvĻŽęšæ§“×6ąņ§Jø0RFR½ń=Ä"Œ“‡žļt?1DŠ“)—=]BE¬Ūuāž:I’$I’¤ŻNĀW>ĄĀ7qƑ•›—Źš¼,Ž=ø.·< tāX!lµB2 ŠšÖŸMäĖM¼11‹łSI©›L«ŌŹV•3ń8åŸMāīoĢžE<9©>+H£y«ŅfÆįO÷5ūīg>N^ĢÕ÷7bny*aŠY8²˜«Æo_='µ]_śÄ".87W¾ŖĒFRIŽT°zN)/[Ÿk¶ó²ŸŠ-ėųżč†,*K%¤źdG%ؾÓß'­d TŌkĪ%—- )5DZ!-ˆlY£ˆn ؓ\Q³“8±¶Kyw#ēü2ƒ’LŖĖ’²tŅ‚)”²vNļßӒOˆ¾T¦äŗõ\żJ#•„RN óŸ/ēĘŃ ćž7I’$I’¤ŻQ››÷ć^¬2tT6·¶YJŹ{KčūėųVkŖöÅz>J’$I’$IJW>J’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’ĀšQ’$I’$IRB>J’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†’$I’$I’ĀšQ’$I’$IRB>J’$I’$IJĆGI’$I’$I aų(I’$I’$)! %I’$I’$%„į£$I’$I’¤„0|”$I’$I’”†Śm<&±łÅŒŗ-W—"I’$I’¤`ųų3ŃņĮĘę3ń|ŗ–Gvu9ß«į/³øo\ˆwņ716æ˜O>̧_PµM» PE(ع½ą^Ė/al~qõēæž4Ź!ļd36昏’¾óź’9͟$I’$I’ĻŽGĒqݾ’VoX;Š 6b!Įwq%JN(Ū²MRĪ$Ī$'‰'{^ŸäÄÉÄyćÉ63Élyń›™—83y“¼¬¶ģȖd-”,‘"%’’H±±±hŻŗ]ļK tU£Żž#4Ŗ«īżŻß½u뢺Z$6K¼  WéæHęĒ_šQ_ī#'ƇyĘσ¾łļRųĪk¼‹7ŠĀÜBĮĖÉÕ?™ĀŁÓ$ßōņĒßJY÷ž&«RłOæ;Į~<ų0”ų@‰š¾ŠŠ?`cnŃrłåf±ŠnYžĘĻč|B!„B!Äę¶éī|¬’e3Ÿ¬Ē™1M2A¬IfŹŽ™ųĶģå'«}ń.Ž–³żóV~ä \ųT!ūėŪk£Ž)`„‹?gŖS9łl5ļ«+SńoO{9YĘ3_©y½ź7ē«“8SŹļŪfH¹'±ŒŸŃł"„B!„BˆĶmÓ->z‡|\’K…/?—ŹÉź4~ģtb%˜œĒOžLg¼‹'"I‚d‚Ą ÷»“Ö½»Äž€yā“ų !„B!„"–6ŻĒ®ßøąē ģ ?÷üæć|ē_ęņk„żä–D·ĻĢĻŁł­_œ¢®|š,uϰB[ó,÷›é\5Pü?²ų»3½$w»ų·źłŃŸ`§uœĪļOó_. yvīs«“UIüŹ—ƒœ¬ŸĮé˜!SC*w¾Æš_ž]¹ķN}??Ųяķõ~yxæqnœ<Ū$Æų“ÆęŅ8©č*ßREæĘÆü¼‡=é£tæęē=–Ł%Ÿ«=˜ĢÆ’VuØ3 6«|÷?§š——lx*ķ|ć•1ź™ĮÄ$ž“NŽiū’¤ū.~ęLĶVķŸÕ-žo™üͳP}Üœ|ń=?Ķ ¶>æöTØ cĒ3x鯆(ÅæšŽ™oõńCæ]„łXĖ„LꗖŌ÷ao’ĀŹļž½=d;½qŽ(Bœ—«ł„4~ćܔXĒxM>t7’Hńӛ/ZŪC!„B!„›ß†ŻłXųļSłż³žČF!×1÷qė‡Q|Ņsęé ¾žĒ£œ*' ƒžRņjOs$Ēæb{_qæńSƒŖ³±RzÖĪļüŲąĀļū¦šćĻŗŁåš’„śĒFJž…?aįė_ķXŲN1+˜õyü‡/ Rā#Ål”ü“)üĒßo‹ŗ|ü~õ+#ģ˚Äd¶RžÉd¾ŗ¤|ރvžļæ›āsuنŸ b±ĒĢĻ|ć!šōÜb‘ŖØ°`&ōYŒęG’f:ŸŃLłü˜0-¹×NAÅ ųĶs’æüŲ ’LŃߟ7{ÜĪ\V߬b Ożž’żÅ‘…ķōĘ9-q^jź`’į׆ٙL×½’ĢÜ=Y¾’d~äĒ}ģĖ™EEåƟ©¼ŚæņŪ³mƒ3üóÕ4TT.7Ļ}›Æ-kń÷é7|¼oJćsfęĻ_ š·ļLówŸ›;VlĻ]\Ii›ä7.f¢¢ņÖ7Ō¹ēW*Y>艪|V÷ż'Ł“ßšńOwŅV”ļÓ;'ø÷ēiüĻwl<øīēžŌĪffJ>Y>ŗōæ¹/CFQ }]ēÓŽ\¼7N *+öżVš)NåÓenĄGó×ķ¼zŪ†ŠŹĶ_õš6‚ÉĪōĒ9-q^Ź68Ģż7Y||9ĄÆž±~,Q偎ć?­ł¢§=„B!„B±5lŲ⣊Ź?~zš/])”ģ Iüé“īČoZĆĢŽžÓ×½ŌąeźķAĪ’nYTĖUżßœ”‰T‚äpāwfłśū¾’żQ~ėG'ĆīĻ4 ć‚ĘĆü¾ü/Ņų‹ÆóÜžIjʧ(.ō³=sńćęé™Įķ,'ūņ$X€$Ū¦¢*ŸŁ þ¹E½Į™Šßy¦SĘ ą„ėfśĀ¢ć–ŸN,@.ĪŖ)ŃKl«,TąlģžćTŽnä­ŽnäӀÜüčā¼–h⬶BƬ•ź—'¢Ź£ŪWOüā±?!„B!„B$¾ ż¶k婞åįIL3C|ܟ¼®}}ķσT¦}ę…Ÿ…IŸĀŒ2÷‘åDˆóŅ»?—ÓR>ćŪW{üā³?!„B!„B$ŗ »Ņ·}&ƒ’õ'£¹ųӟ+ćZ“ Źßz#…O:†˜żČÅoż«Jnłżß€2O’Ē>¾ń1|H©Éę'žĢÄO–±ū¼ ¾•«yW„ówvĶŅö÷I¼ŁdŹK’ †}RGr ½{mģ Ģ02”bxłRoŒÓEe¤R¼gޟū˜śšn+e€śŚ¶kŽ_"˜ńĶ-źf$­ü]zO€~Ģą„łKš+Ƥ…Łƒ}.wāgs ŌĻiH2Ń÷T&;@oDs\Ć⧁ŃūB!„B!DāŪ°;ÓϘ(¼ēā~“æō¬·h\ų–/–csńķoīĄžŒ™ć?¤pü OÖé’² ŪgRųŹOĶP–¦Zfø:d Eē"HņPŃGßń›æ{faåWž_ż£¤¾|_žŚuOXł/æ9MLŒńįG©†—^n›[Ŗüy?uČOį~_’’—,f± Ŗ¼Ņŗ 5ź{ōÖ*8™’x_rghnż\=ØšŁüŠo„Nkõšvw&`£īĖӜ?ź2*MŌ}ŽĘožs¶¹/t‰wœgJņųņo»©©·ņÕßņG—QרųĶ[+_¢ŁŸB!„B!6· »ó1łuæńń‘Ū“®;›ŽģGAş™Ē äć<¾…ß„6örųóÕŗöē-IęSæ:˧~Ē× £ÉVœI.`––·3uķ+ūę$ŪÅļ— ö…\¾yj OV2å郬¶ĪkK"ć &žģ n”G ²ŽWMü­Ū бåøųoƒÜü›TöešÅæę§ńcBĘyėßņńlh9?üŲūĄŸ_Ä5NįSLĶ.~łS•“،æEķG^OęēJē>FlSꞕ™|®ˆ7?ē‚LüÓē¾ŗcaūĘ×Uʙɏ*ä×/ĶšØ>Ģć.žd’^±˜ł»oęÓ’-…ź2…/’Ķ4æ¤ś°(sP6ŃĖWēöļ8ÜÉäž‚Ā’ų…„y;nÕU>½Ē5*~ó"勎ż !„B!„bsŪ°;¼āē£ńõ-< 3s’ļō_µQ§xłZ]IŲ2­'MćösķĀž"_÷žŽ~ÖĆ__ĻadœÅÓ$ß{ČžYįźĻ|üč_łz÷żI˜ńŅóš—ÆüīŽ…8]¾”k~įgSųē¦l&HĀŖé˜įŪ?›Ćo]\łmį¦?ć_æå ×—„ HVT2ŅUL1śh¬%ÉD²¢’¬ØĶꞁ8÷šB†5ōĪ8Ė_¹łķrpϟ²P¾¤Lu!ڶ7'ųW?‘Ź?\ĻÄåK!YQ±1Ćh‡ü×ķ|€yīqŽsņ‡=|õÆsčō'aĮ÷(*~Ƶ|zkTüęEʽūB!„B!Äę¦TUUG÷łg±.O¾‘Ī•öa»čāČ/ź»[S!„B!„BˆĶ`CæķZ!„B!„BńųÅG!„B!„B!DLČā£B!„B!„"&䙏B!„B!„Bˆ˜;…B!„B!„1!‹B!„B!„Bˆ˜ÅĒg+;͹Ļ?MuŹćóéųć.šL]Z¼‹Ѷšē8÷¹£v<½qŁčņÅĖfɗhyžŸ¬Ķ‰w1Äc`+÷#!ŒņøœWē=nõŻHö²Ó|īó'uĶסּ*N¼pOT%Űd"=Ž×ƒF’ž&DbÅĒsą‡/š™CłK^QżŪ\VÖC;e“TYUeķ‚Ś²«Ųµ§Ōć鍋–ņEkyūī9{óēĻsžüy.<’iĪ>óµe19örzābd{¬‡Öž”( (?Ń\O’›ÓfwcIņ>¾ā5>ė9n,Ļ«‰h£ē9į$z挶|ŠŹ¦Č„ǽ}GlN҉2/ÖJś›Wln›rń±p{f¼‹Ʌ§qfŽŅŻŚĖV©G$CĶÆšā·ß_s«£Œš=%T¢PZŹpķ«( ʌ‹Ž†š;ūp«ŪØ:ų ž;œXŹx¶Ē¼Dļ‰^>!bAņ>žā5>ė9n¬Ī«‰*ŽóœDļ—]>愍÷¾ł-Žh›ŁćĮćŻ¾ƒD˜k%żMŽ+6æM·ųh/;ĶGvÄ»1Q¶+Ó`' ć›®YBl•zˆšVm_’ -“Üü«o½ÄÕ/)eõvšćSŠ•čż#ŃĖ'D,HŽ ‘x½_&złÄOč!ł"Äęg‰wōŠĆ'7„%ķ„Žśßė æA²“]µ»©rd4ń°łWŪ'C61ŪJ©9ZIqF&)Š©qķ M“ęŽa©āägw¢Žwa**"uvŪW»HŪ˜2»ńök¼Ū2²ĻŠORåĢ ÅÓn7®–4ŗ¦¢ŖGRf ū–’›eĮä¦ć– ū‘C8ŗŽę•¦Ń0{[¬×”3qĢŽāå7Z^ÆÜ{Œ‚āL²M©X×Yßōāg9stšī÷ĘIŚUJn¶Š2ķ¦§ae}œ=Oé£g†ØAßłēV”¹žłó”[T“ e6ƒsēĪ-ünüī+\l^ü+Z¤z荋–ņĶæi¾ųf½ “ńĮµžU1O—p}ü*CÅ/ą,w‚+tŸZņ*šņĶæoy\ō“G¤~śņEküĀõÕDŠß‘ēχķWéÅĻņĢŃ m/_äöŌŹŸŃµÄO-yTzšē›i»ŌŽ©j7e™©«Ž“Zś›žö6O—;pö¶ļ+ćą©l†;›ińTS·s/“\ZŲߎ3ϳ3ŻCwė†<YXEń‰ÓŲ/æĮ•¾š¬«ÕĆb©āČé=dĶöŅŻŌ‡'¹ˆźżu˜m ³ĮqčéCä϶ņŻ7Ś÷—vˆŠŹT¦{]“¹™ fQXVJķÓYšöEŚĘuÖWQ°Øłģ8b£½į —Ē2+ź©=qŽz…ƑŁ£ł£Kōؐµćūœį ÷żwéUĮ^|˜•3ÜæŅHÆnŪįįÅUK=ōÄEkłźĻ¦Ģ4@ūŻqū¬¤flc[y°śÉk­< ghņ²!ūԚWєĀĒEk{@ä~čŹ-ń3§VqüŌ2¦{é¼Ł'ÉIå‘Z¬V•å‘Öæ¾±ŪKŠ`ŁEl~If÷Ͱ k•OO’Õ?“䁪(˜°Sq°”®Ū×¹Ō¤[s˜ŗż'Øx•†‰¹öŠÜßt“o“yŗÜČĆåYł@ųÅGGN*꩎…ŸõÄYQlGy¢Ü‡«õ:[Z9Ž4+¬Č°9«/šź«1~zĘæ”Üć<łt!ÉcčlébT,GeåZ\śĒ—„$ļć—÷ o|6ņü”ēøZĪ«šŪcó”ÕhĶEYłŌ8¦eĻB3ržĘöK­õ]؟†ńOėų¢µ|ZĒ%˜EÅQ;cżmÜķ²‘QZEå”S$_äśĆÅżvßxo—³RJĶ“„XV¹CkœµŌWOūnÄ|r=¶ŹxjŌõ ľ}&żmŃćvžÕJė8näuރžŪĖwĄ²ÅǬJI3=ŗ!ńĒ«õ’ÅGĢµŚI’ DVÉŽõ¼ĀÕ¾`¤·®É’¼›Š•‘†¶°æW‚iųś_ēRĆÜJöż/§>[K^q:4Ͻ–Uł Īō :~š.Cs ŅŪćåÉēźØ¬Ķ”ķƒÅEˆńÖFŚŗ‚Ģä99ø­—w›:0„$±½“„ZG¦aY•ϰ3ŻĆw.Ņ<ö(įŗļ1i{žś½•ŠwWW=2k+É3 pėĶ÷ió*@7ƒćĻrś°mĶŲԟżō£ ą[Ø,ž³“ņśw–nŁMo—cĻÕQµ#¶†ÅU-õēnæBS×Ü 7žŃE2ņ/°£¶„Ę·{¶™f oõ«„锦©ģ(~ū†ŻNO=“ÄEkł¬Iµ8Ņg¹v…¦®łü½GĖ­Uß1OĆńś@ݶŲĘZó*šņĮźqŃŚzśhĖ—ykÅÆ ŗ†lĖ·®Ģ÷®Gż#ōö“ĘoØ}ßÉķŌ„ß\˜ZĢUäŖø›„­»żWoü"ћī¶ė ķįžąū8ó/ą(ĪX'õö·Hķmž†3<:…Z”½źļÓsmxĪÅ/š8'gųøõś„GķŠĻj÷›¬Öt·G„ųéišCNŅÜwxńĶŪ Æõ“ßćę’wĒė¼%yævł"Ń3>yžŠz\Šv^©=¢­Ęčöcē9F÷ĖhźiüÓ2¾h-Ÿžń@QmųūßćҵGÆŻ»g/PT[ ?Xüc“ŠŹšŠ0–@>Ā_¼é‰³–śjmߍšOFk+§F^Ʋ}&żmŃćxžÕJė<ŃČ뼁{}xŖ 8Vh Y*É·15Š©»‰>^Aš ƒÕžCŚ’uEMĀžc]÷~3w—’=ŪĖ»m¾°æWM“ŒtøQż7ėog|RĮnO]ŲʑŸ Sh2…l×=)yE‹ūR|x>TT>PfüØØ˜&üųU+é¶¹ g£ø;i[ܟŠŹ`×Ŗ}›īzęŪ1= Ķ«,ģĖćźdr•y­bĶgĻӟ¦Ā<vM ó߬æQ?XŅ㢵¾óqīń†ģs“χ%k{ŲcÆeł6K÷M=“ĘEkłšš©6Ró+ŽæšHyŽÉ§€j£ZљW:˧5_–’¼|;­ż“ē‹–ųeŚ4õ­ń›é{÷LŽŠō…×LÅEd›Fé{0®»|Zūƞųi¢#TŃ{ “=¼>BĘI=żMSūFяV3Ó?ʤ%‹ŗō¹÷V»Ąłgö`õW‘‘6ĆŌ€ˆ.΁į»!ķ®œĒ=ķ”!~ZŪƒ~¼ŒFښþg^¼Ī[’÷ŃēżŅņ…+ļRFŸ?“7ܶ«ÖEC{čEdp{€±ó£ūe4õ]küÓ:¾h-ŸŽyÄȽ”ćõ÷ū°dƌK¤öŠg­õÕÜ?6h>µ-4žz=Ćö5šō·%ĆóÆZćlōuŽģÄ †'’Č«p,¼–œ{Œ¼¤IśīčÆH¢W;uš ņPÉ%ēQū«¦IĘū½Ąś¾Pc÷v;3m«v8?^æ²äēGĻįYrSTr˜&½+:²'ąkZČž”G­˜AÅö˜) Zź9w®>ōŠ‚jž„:Y„u:t¦¼V=ģVšO†ü.hĒė«#%Ģń3KźČD@*µéĮĢs”`Wµ“,»ł5DŤ¢øCļÓR_Uń†ÄĄć  Z­T+*­j“WkÓZŠ-ĮīŽ+åąŽœĻŸdlĢ͘»šŗW}O¤< 'hSAńѦšUs^é-ŸQqŃӏōꋿCküTT:G}É+ęž™_œAŅč]šŻfS#ʧ'~Zč̓åķ”ć$hļoZŚ7š~“šŁ‰LĶ\ ;× Ar·łš)9T'«“oĖ$Gńp4˜¢Šs`ŁöįDźGzꫵhiKZ:v| LĪÕ5ń:oIŽGŸ÷zÄźüa“Hķ”w>I¼ė‰Ńż2šś®5ži_“–O×<?cŽŠcz<^TKjŲø¬Ekœ£©ļZ6j>¹QåKäńŌčėA-a|‘ž¶HĪæįi³Ń×y**ŻŻ^Jwīę>z]™‡mŖƒĘįš×[kIōńŹ›~ńњTĖń³¹Œ^~—¦‘Ų>7dÖŪLĆ©Ų[Hj’‡‘›7ø6l^ł²*Ÿ™ūųĶŻ Öµ£uņ›Ū€]!Æś¹Ņ¶ņ.)Åēgp*ōĮAŃŌcĶIĒ“‹†wŗÉ:yœ’£ūh|3ō~㬊3X>Ś-7˜DčGŗŚCCüÖ;އ×ó–’÷+ė«GLĪ `µ¼ŠG¾Ä‚‘ż2šśj’4ڐy»āj×zāl”šOnTł"‰ēxń.Ÿō·Pńl­zž]•Ęė¼ŃŽxö”pÜiāŠ+HI¾éīΨ拉>^aS.>*Į4öķ(ĘķW0)^ū‹Ø9ń$¼ćHF{np±'ōµõŲQ‘ĶģƒĖ“z×7HMC0{åĒtķųĆ?70IJcOƒÕ¦248¤iūHõšųĮjOcžÆfK%©¶š™<öą6÷&&HžøĒ“å<]×ÉŪ‹õ()ÉĮ܍7Æ-żKĄΤƒkTrõņϽd%+5S‹w²¦¤[Pü~īĪĘę$¢·‘⢋>÷ZŻhR¶ąōéķœ.¹Ī;Ż”Ļ16OóŅavdxįg]y„£|FÅEO?Ņ“/Fõ=ń›ķy€{ŗŽ¢āīM 3ł!½ć„ū‹ QåÓ?橕;W&¹’čgUńćs|y‰žž¦¹}5–OK}‡GżģÉŪFAI6žĮ÷éĶ”,?)ĢŽ-l»īń~šś‘Ęśj‰ŸÖöLzšNVš†Woóx·$ļח÷ZÅźü±Ńō̇ŒĢÓ£gų/?Œ%ŗGMjft朏Qķ«u|ŃZ¾õĪ#ģöT’¤īł–Ö8ė­Æ&0ŸÜØņEĻńT‹˜Ģā8žJ æļnD?’js,®óSō WQ^^Dšŗ¼5®·4Iōńj6Ż3}ŠB ˜EŁž:źźkŁW·‹Ŗm©(ÖŹŹ‘w@’ Nį̘¤«Õµīƒ”"j‹‹ÆfK%%¹05ž &Öāc6ƌZĒ£;;—’[BK=ś<³ŠØJ^ģ4)EE!ĻŠ „‚Óż7hź Sz˜ŗŒÅ÷“·™’—ä<ĘvKōĖņJ0 ĒŽŠ*É·ėŗ}ü~?ØÖz/„ækĒE·%ĒõOLĪ=`>9t“hó“°ž‡É³Ķ0|ßµššž¼ŅZ¾9Śā©=ōō#­łm’Hu–Æčzāē7·įõ‘QVɎā^­Ć¦»óŃw’¼t?ōµŅ'ž£.å>/]Ųų“Ł:”īŹĒ4ō1 ėų(ݼ±¶7é«xŹĆ§±ŻécJv–‘eąö͇č=Ąųķ7¹—’*?KJg#£ Įģ ņŅ8,ķ¼üīb#h©ĒŲĶ{ mÆ„ęÄ1,­.¼ÉŪŁQ”įéF ō½’*®³/Pqd¾ĮŖĄĆĪņ:N“Ņ=Ą–VJe‰€V>)QUńavįŌ”Vŗ&į(® (i‚Ž+}!Ū9¹d¤« ZŁ“Ć IAU¼Œ M…lģeJ­¤āh 3wŻųčėė_ų}ŌõX%.ZĖgN?Ą™c™Lō¹›ĀoÉ `g فA̲“!5ē©Åɾ6°¦™[ȶܼ—yϵ8ĄkĶ+=åÓ-ķ”§iͽż#„Ӆ'ŁI厕g=żzŻų–S•leŗ·+ģńģæFCQēĮ*ōō7-ķktł££LZ*ÉĖ|Ą‹#f ™”ą T§Ķ00čfžłĀFĒy…Uś‘žśj‰ŸęöP åF?yOķäSOŪééīcĢg#³ „BKÆ_ū¾x·$ļ×W¾y‘ĘēX?"ō÷#‰z>“ ­õ Ž`Š÷EuĒ8˜įb̜GIq&,’°•‘ó£ū„įł§q|ŃZ>½óKįNl£{P!·b'%éōŻŠ’|4ĶqÖQ_ˆÜ¾1™OčqOõžŒl_£IŪøöHäóÆ&ć‹ė<Ļ=£u58 Ą}§sĶf(ČŪŽ3ŁJ•émĮŠ }¼2¦[|\ssJ ÅÖ6^śAū¦Zx4§¤4o†‘+÷ +÷µ·>dĻ”ķ©£Tń15ŽĒķ«MÜõFw€¦·^Ā{č4„ĪŻ8«av2€w|ˆÖŽŻõ˜õ·rż’•żū+Ø8’‡it˜ūļßĮūä!r×ų˜y7śÉ|j'O×=ąķĘ1†?z•ė¦Óģ)¬ē°ÓĒä“‹ū×ļ\{šŖØj ŖiŒĪ÷ū±ļ­¦¶ŌŠ2å¦ćź †Ļņj~ˆS5ó¤UP”Tv=•Ē. Øvšo7„ģ30ŻČG¦±oW‡ŽĻ}¶i¬åeŽj™ūüÓzė±<.ZĖ7;ń½§q:«pTŁHņū˜wqūjžÅ†Ō•§)łTķŁ†ā÷į¦ććī»Wl¦%Æ“–Ok\ęEjŠŽ“䋮žQ ˜’ś<÷0÷ÆßÄ~ä¹QÄoŽTo:ČOž¤§ĆĶņ›Żcэ‡Ö›ĖééoZŚ×čņ¦óW‘>¾x·šhŸµzŒįžŠ¶3z¼gy?ŅS_-ńÓÓŽįĖ\~«šš=”ÕlĒŖųšL Ós'ōŪćuŽ’¼¾|ó“ŒĻ±8D:®Žó~$ė­ŲŸŽś6\kĒ¶æ˜’=Y8Ę]t]nĀ~ņ8KæćÓčyÜ/cZĘ=ó!=óˆūōcŪ[Mm™eŹĶż«7hDU­ó­ć)DnߘĢ' ō8ާzĪGFµÆŃ¤æml{$źłW­q6ņ:ož?ŲL÷ĆŻdå>d c -.·E¢WFPŖŖŖc’Ucb…ķ'ŸēHV'/~ļf¼‹².ė­Ē‰.Ņņ=޼­1RzÉ'xö‰išæy‰»ń}TBŁ*yj4­ł’čńŪ*ż×h2¬O¢ĒOņ>¼Do·­n«ę•V[ż|i³„Š'>W‹„ń{¼Ó¶¹ņ$ā·‘d<]ŸDȗĶÜßÄśDÓ+O¼@żc¾õś½Ų.6r¼Ś|w>nęįnķ)ėõ IDATµŽ‰w1Öm=õHĪy‚lÅĆ`æ—łƟʦŗ…6ʶJžĘĪŚł’čńŪzż×h2¬ObĘOņ>’Äl·­ģńČ«µmåó„Q””,Ņ“žĶ—'‰æųń4‰/›¹æ £hėæę”¶Ŗ 7­ē¹ė‰ öć•,>ĘI×}ÓITzźQwęę!×Üó‚’ó(ŽW„i؁k#2 'Ŗ­’§ń’čń“ž+G’÷"Ž$ÆVŚJēK#YÓ )̱‚jĮQY€=0Äk®Ķ—'‰Ž¾"±H›5­gV¹U•äĢŗx±u:ŽEJx²ų(6ĢØŪB•sĪ*<Œ4ņŅ›ļÖd!GŅÅćHņ^Ă䕩ŹV°‡Cµų§‡čø~#Ī%Š]ǟ¦Ą$0»ś3ʂ¦Q¶iŚīŹ»·Wż½0ĪVj·­Šß“¶‡ōõK+ŲĶĮŚLüÓCÜæ|=ŽÅŁ䙏B!„B!„Bˆ˜ˆüU&8[ŁiĪ}žiŖSŸOĒæpgźŅā]Œˆ¶Õ<ǹĻݰćéĖF—/^6K¾DćČóųdmN¼‹![¹ a”Ēå¼:ļq«ļF²—ęsŸ?©k~o Tqā… |¢*)†%‰čq¼4š\G ²ų˜`üš>s(É+ ±žŹóXXYķ”MReUQP”µ jĖ®bמRCާ7.ZŹ­åķ»ēģΟ?Ļłóē¹šü§9ūĢŌ–eÄäŲĖ鉋‘ķ±Zū‡¢( lüDs=żWlN›e܍%ÉūųŠ×ų¬ēø±<Æ&¢žē„“čż2Śņ)*›"—÷öM±9I'ʼX«¼¾ŒĒxæ΃B¬Ē¦\|,ܞļ"ÄDrįiœ™£t·öÅ»(ė²UźÉPó+¼ųķ÷×ÜĘź(£fOÉ•(”–ņE#\ū*Š‚2㢣”…ęĪ>Üź6Ŗ~‚ē'ք2žķ1/ŃūG¢—OˆX¼æxĻzŽ«ój¢Š÷<'ŃūåF—Ļoić½o~‹7Śf6äxšx·ļć ęÅZmt¾Äc¼ß ēA!ÖcÓ->ŚĖNóđń.FL”ķŹĆ4ŲIĆų¦k–[„"¼UŪ×?HCG -7?äź[/qµĆKJY=‡ęų4A%z’Hōņ  ’÷B$žDļ—‰^¾D'ńzH¾±łYā]½āš Ä aI;D©c†ž÷ŗĀoģdWķnŖ™Ķcŗ¼}ć3wžŠs\-ēU=ķķ|h5zņåöĖi[2ž8{ēÄ%¾÷ސ®śĘ¢Ż–Z«_źÉ?­ćŸÖń%šņ­5X3Ź8p¬gŖ‚nŗ›oŠŌzĢŅC8P\ųyā櫼Ł>¶\Z㩾ė™7=Ÿ\Æ-5žp=±o_£5,_“×QŃ^¬f3œµJōž&ākÓ->Ę[īīg9¶3«āĒŻ{·® ²ßĀżNģS¼ęBPĮJÅŃZ<#]ÜiĘQVJażqź^£qbqyųĢaœÉōÜmbhĘNńĪö±ĆkļŅ6„ Š’DF.ܹÕĒö}e<•Ķpg3-žjźvī…–K ūŪ{ęyv¦{čn½Ć2 «(>qūå7øŅž‚uµzX,U9½‡¬Ł^ŗ›śš$Q½æ³Ma6Āć4=}ˆüŁV¾ūFŪāžŅQQ™ŹtƋö17SĮ, ĖJ©}: ޾HŪøĪś* 5ŸGl“7\įņ˜BfE=µ'NĆ[ÆŠ8²ųW¶ę.Ń£B֎Cģs†/üŻ÷ß„W{ńaTĪp’J#½ž¹m‡‡r-õŠ­åØ?s˜2Óķw?Äķ³’š±måyĄź'ėµņ4œ” ČĖr„ģSk^ES>­ķ‘ū +_“ÄϜZÅńS{Șī„óf?ž$'•Gj±ZU–GZKüśĘl/)‚e±ł%Y˜Ż7Ć.<®U>=żWSütŠ’Ŗ¢`ĀNÅĮRŗn_ēR“BnĶaźöŸ ~ąU&ęŚCsÓѾŃęér#”gåį9©˜§:~ÖgEQ°å‰r®Öėtxliå8Ҭ°"Ćę¬6¾hŖÆĘųé’RróäӅ$= ³„‹Q²e”•;hqé_–’¼_ރ¾ńŁČó‡žćj9Æjnu̇V£5_eå#ĻL˜–=×ĢČyŪ/µÖw”~Ę?­ć‹Öņi”`GķŒõ·q·ĖFFi•‡N‘4~‘ė÷Ū}ćE¼]ĢJ)5O–bYåN ­qÖR_=ķ»óÉõŲ*ć©Q׃ūö5šQćĮRėŗŽŠņ:`5›į<ØU¢÷7_²ų؃9£–C»3IR’Č*9±žWøŚŒōÖ5Y’wSQ 2ŅŠö÷J0 _’ė\j˜ūKĄż/§>[K^q:4Ͻ–Uł Īō :~š.Cs¬·ĒĖ“ĻÕQY›CŪ‹‹歍“u™Ésrp[/ļ6u`JIb{i µŽ MĆ&²*Ÿagŗ‡;ļ\¤yģQ‡ķ¾Ē¤ķyź÷VBß]]õȬ­$Ļ4Ą­7ß§Ķ«Ż Ž?Ėéƶ5cSöӏN ·PYœlĶN~ČėßYŗe7½=^Ž=WGՎtŚ’j¢„¾óÜķWhźšlĒ?ŗHFžvŌ–ŠųvļĀ63ƒĆĢä­~•0=4Ā40•ÅOcßưŪ驇–øh-Ÿ5©Gś #×®ŠŌ5Ÿæ÷h¹µź["ęi8^ØŪŪXk^ES>X=.ZŪCO?mł2o­ųTאmąÖ•łžŃõØ„~£„Öų µć;¹ŗō› R‹¹ŠüBwóƒ°u7¢’ź_$zóĄŻv}”=Ü|gžÅ 椎ž©}£ĶÓp†G§P‹²Wż}z® ļùųEēä ·^æōØżśYķ~“Õś‘īöˆ?=ķQsČIšū/¾y{įµžö{Ü\ņīx·$ļ×._$zĘg#ĻZ ŚĪ«ó"µG“ó”ÕŻ`ģ<Ēč~M}#ZĘ­åÓ3(Ŗ ’{\ŗöčµ{wąģŠjKį‹lRQĘČĀGų‹7=qÖR_­ķ»QóÉhm„ńŌČėĮX¶ÆŃŒ`ż×Qóō\¬e3œµHōž&āO–u°ŚsH[2>)jöėŗ÷›¹»”ģŁ^Žmó…ż½jšd¤ĆśčæY;ć“ v{źĀ6Žül˜z@ć)d»īHÉ+ZܗāĆš”¢š2ćGEÅ4įĒÆZI·ĶU° 8ÅŻIóŲāžTT»¦PķŪt×£0ߎiģm^ea_W'“«żAÉšĻž§?M…y8ģ‰A ó߬æQ?XŅ㢵¾óqīń†ģs“χ%k{ŲcÆeł6K÷M=“ĘEkłšš©6Ró+ŽæšHyŽÉ§€j£ZљW:˧5_–’¼|;­ż“ē‹–ųeŚ4õ­ń›é{÷LŽŠō…×LÅEd›Fé{0®»|Zūƞųi¢#TŃ{ “=¼>BĘI=żMSūFяV3Ó?ʤ%‹ŗō¹÷V»Ąłgö`õW‘‘6ĆŌ€ˆ.΁į»!ķ®œĒ=ķ”!~ZŪƒ~¼ŒFښþg^¼Ī[’÷ŃēżŅņ…+ļRFŸ?“7ܶ«ÖEC{čEdp{€±ó£ūe4õ]küÓ:¾h-ŸŽyÄȽ”ćõ÷ū°dƌK¤öŠg­õÕÜ?6h>µ-4žz=Ćö5š‘ća×QčæXs_Ū#žēAM½æ‰ø“;uš ņPÉ%ēQ’QM“Œ÷{õ}”Ęīķvf:ŚVķč*~¼~eÉĻžC±ä¦Øä 0MzW ž€¬i!ūS=&fŲc¦d€j©ēܹśŠ_( Ŗy–źd•Ö驙ņZõ°[Į?<ņ»` ÆÆŽ”0ĒĻ,©#©Ō¦C>N0ĻQ~€]ÕN²ģ6ęדŠā½[LK}TÅg/€jµR­Ø“ŖŃ^¬Mk=@[\“[ø{Ɣƒ;Np>’±17cī>>jź^õ=‘ņ4œ MÅG›jTĶy„·|FÅEO?Ņ›/Fō­ńSQéõq$Ƙū+h~qI£wiv›!LŒ(Ÿžųi”7–·„Ž“ ½æiißhśŃjf'n05sģ\3LÉŻęƧäP¬Ņ¾-“ÅĆżŃ`Š*΁eŪ‡©é©ÆÖž”„=,iéŲń109W’ÕÄė¼%y}Žė«ó‡Ń"µ‡ŽłP$ń®o$F÷Ėhź»Öų§u|ŃZ>]óüŒyCéńxQ-©ać²­qަ¾kŁØłäF•/‘ĒS£ÆµH„ńÅČńĄØėˆĻuc¢Ÿ½æ‰ųŪō‹Ö¤ZŽŸĶeōņ»4Äö¹!³Žf>HåĄŽBR“<ŒÜ¼ĮµaóŹččUłĢÜĒoīN°®­“ßÜģ y-Š×ȕ¶•wI)>?ƒS”ЦkN:¦]4¼ÓMÖÉ支Gć›”÷kgUœį‰z;ÓŻ-ÜŗåĘćS0ķ‡R®įŲįź‹ŗś]¬ŠBøõšuÓ]qŃJEÅõŃ« ß+¢ĢYˆĆ‘ĒöŹ"J…|ūū+¬mž¦Ś@™ō…“µ–¼Ņ[>£ā¢‹Ž|1²hķ—Cķć̜*bwr ĶÓ&ʲmŒu?»_ĆūÆÖŁjŽžh-źÜŽ‚Ź÷¾śÜ"Ży®ž¦”}õ”/R}UT†& Ż£9”åēį …ŁŃ”…m×=ŽÆBS?ŅX_-ńÓŚIŅÉJ³ĄšźmÆó–äżśņ^«X?6šžł‘łbzōŻĖc‰īQ“šŻ/ēßcTūj_“–o½ó»=Å?©{¾„5Īzė«ÉĢ'7Ŗ|‘Äs<Õ"&óƒ8ާFF^GÅćŗqSœ½æ‰øŚtĻ|ō) `e{ꨫÆe_Ż.Ŗ¶„¢Xs(+wÄ»xŗ$œĀ™1IW«kŻbxp RŠØu,.¾š-•”äĀŌPų/˜X‹kpŒŁ¼2jīģ\žo -õčšĢ*¢*yqŠI)* y†f(˜īæASO€œŅĆŌe,¾7˜¼øĶüæ$ē1¶[¢’³†LƱ#4‡JņmĘz£næßŖ5¤ŽKéÆĒŚqŃmÉqż“sXNŻ$Ś<-¬’aņl3 ßw-¼¦'Æ“–o޶øDj=żHk¾DŪ?Rå+ś‡žųłĶmøF}d”U²£8›w÷Ź!ßČž«kR`ų‹ž¹z‡VĻ/Ķy°6=żM×x „|ź;9ąĘg/bG¾…‰7SƒCLåUSšcĆ3ŗų-ŲF÷‹4Ž/ź«%~ZŪ#0~į‰$2*kÖ,}¼Ī[’÷ėĖūy‘ĘēX?"×hŗęCę‹ov’€j'3gń¾ƒäÜcl³®ļv#ŪM×|Ć ž”u|ŃZ¾õĪ# lFuĻ;µĘYO}AG’ˆį|Ņ[a<Õ@ļüĄČö5ZlĘ㮣āqݘēĮŅcOpäųε7Jōž&āfÓŻłč»’^ŗśZéĻQ—rŸ—®lŖä*Ż•ičcÖńQŗycmoŅWń•‡Oc»ÓĒ )”ģ,#Ė4Ąķ›Ń{€ńŪor/’3T–”Ī6FF‚Łä„;pXŚyłŻÅFŠR±›÷Ś^K͉cXZ]x“·³£BĆӍč{’U\g_ āČ>}3_’€‡åuœi Ēm<Õ{>2²}Óń`×Q ż:@D>Ī+ČŪŽ3ŁJ•émĮŠż$zń·é—ē¬9„†bk/ż }S-<šÓRš7Ćȕū†•ūŚ[²ē‰ ŹöŌQŖų˜ļćöÕ&īz£;@Ó[/į=tšRēnœÕ0;Ą;>DkĒ€īzĢś[¹~ÉŹžżTÉĆ4:Ģż÷ļą}ņ¹k| ̼Ężd>µ“§ėšvćýŹuÓiöÖsŲécrŚÅżėwH®=MUTµÕ4FēūżŲ÷VS[jE™rÓqõ Ƌ‹gy5?Ä©šł6+(J*»žŹcT;ųĪ·Bö˜nä£ÓŲ·«ŠCĒē>Ū4Öņ2oµĢ}ži½õX­å›ųˆŽ‰Ó8U8Ŗl$ł}L»ø}µ‰ĻbCźŹÓ”|ŖölCńūšN Óńq ÷Ż+6ӒWZ˧5.ó"µhļGZņEo’Ø?PLI}Š{˜ū×ob?rˆÜ(ā7oŖ·‡ä'OŅÓįfłĶī±čæFŽCė̓åōō7-ķktłӍŒł«H_¼[x“χZ=ĘphŪ=އ³¼驯–ųéiļše.æUMĶž Źj¶cU|x¦†é¹3²]¼Ī[’÷їož–ń9ēHĒÕ{Žd½ó”ūÓQ߆kķŲöS²' Ēø‹®ĖMŲOgéw0=Ļƒūe ņOĖų¢g>¤gq’ƒ~l{«©-³ L¹¹õ ĆØź”u~ u<…Čķ“ł¤ĒńTĻłČØö5ŚFŃ^G¶öÕ+Qσį„ūųl¢÷7JUUµ<~3¶Ÿ|ž#Y¼ų½›ń.Źŗ¬·'^ø@JĖ÷xóöźƒŪFH/łĻ>1Mó7/q7¾ƒJ([%O¦5_=~[„’MʃõIōųIއ—čķ¶ÕmÕ¼Ņj«Ÿ/`¶TńÄēj±4~wŚ6Wž$Bü6’Œ§ė“čł"ķ›X¤=6ĶwēćaīįVėxcŻÖSäœ'ČV< ö{™’8aü©lŖ[hcl«äiģ¬/‰æ­×&ćĮś$fü$ļ#IĢvŪŹ¼ZŪV>_EIÉ"Mń1éŁ|y’ń‹O£±yņEŚ7±H{$:Y|Œ“®;ś>¦“ØōŌ£īĢ ĢC®¹ēe$ēQ¼ÆÓP×F6ןq²Uņ4^=~ŅÅćHņ^ěäÕJ[é|i$kZ!…9VP-8* °†x͵łņ$ŃŪW$É!¶&Y|fŌm”ʹg• Ęyéƒ{ń.–Béæāq$y/bAņJhe+ŲĆ”Ś üÓCt\æēÅĪ®ćOS` ˜]żmAÓ(Ū4mwåŻŪ«ž^GŚ-<­qŁ*õB yę£B!„B!„"&Ā}Q‘B!„B!„B¬›,> !„B!„B!bB…B!„B!„1!‹B!„B!„Bˆ˜ÅG!„B!„B!DLČā£B!„B!„"&dńQ!„B!„B²ų(„B!„B!„ˆ Y|B!„B!„BĄ,> !„B!„B!bB…B!„B!„1!‹B!„B!„Bˆ˜ÅG!„B!„B!DLČā£B!„B!„"&dńQ!„B!„B²ų(„B!„B!„ˆ Y|B!„B!„BĄ,> !„B!„B!bB…B!„B!„1!‹B!„B!„Bˆ˜ÅG!„B!„B!DLČā£B!„B!„"&dńQ!6[vūv—Ę»B±įdüB!„ŲœdńQ„e+;͹Ļ?MuŠļ¢ŪjžćÜēŽ®k±čG¾Ń6¬euĆъµ”Œ4š­w:bY †-»ŠNwnw­²…ņčŸŲŹ"ēĮÖ:®Ŗ¢ (k÷xõ£ęĖ]dž9Ź‘œ7øöpvÅļw=uū¾żņķ×w<}śŌ^|łc’ OóÉc¹KŠėĆļõ2ō ÷›ŗ^Žsö;SēPæw×½fš:ĒƖĻb©āČgkÉW&éxēU‡’΄缰¢|KØ3wWŌO‹${{”—™…]™Įš2:ÜÅ×ļ-l³ēģvŚgnz“wŪ¦^/<ņŠ\_Šg­åÓk3ō·Döø“~ųŪG.óŅø–c^¤ńO!„B$ž˜.>>l”Å7wEµā(«&/é!]÷ūšųm4=Ė"ˆUXeŌģ™ŽūEˆÆxåĮfČæ”ęWx±yķmāUéńF>Īāĉųī­•¬¶pŗā5L~Ę»šéUĄ’JV^…U‡y>ŪĀwżaHQ”™Ükq3cŸŪ¦ź`Ey—yõśŹ K^¹3Óé8Š2`hrįwśĪ sås»:qBŽ”Īj Uh¹’ė8ńLi\ŻĶ“ū¬deeį(ÜĶń¢V®<0/֗$ņŹJ”ķnHüĀÅu­ś.­G¤8k-Ÿ^›”æ%²ĒyœL.<3s”ī«}$ʇe"ŽB!„"įÄtńqŖÆ–¾ÅŸw8«q˜G¹ŃŌĖĆ !Ė7Śõß3hMb¼£•ö‘G [wš(=ņ–VQŸŽNĆÄ£’ ów©ˆs’s=źŸ?O¹EE1™Pf38wīÜĀļĘļ¾ĀÅꙩ7$;ŁU»›*G&Aó›Æqµ}兵‘ń‹#ņ ĒM*=Ķs‡ĶÜ~ł"mKŚūĄŁó8'.ń½÷†¶k»ŌŽ©j7e™©k¶G$zó@K\–ē½oÖĖŠH\ė‰źøZö§Å³ē)y‹ļ^ ū{{å³|bĻßłē¶/}ōŒF5čZx}©Xõ£DŃ78%éŲÓU˜æėćW*~g¹\”mRP`cŖå}žfʊØ6ߣu6žqČ“Ū0j¼cŅŻIæi'…»sių`ķń)Śś.³®ņi 7OÆęJ„I* IDAT9ž&eÖ°’`)¹YLīa:n¹°9“ā>sgņ=w›š±S¼³„}gģšŚ»!:z(Š‚­ą(O”ūpµ^§Ć£`K+Ē‘fęöžyžéŗ[ļ0äĢĀ*ŠOœĘ~ł ®ō͢ВDF.ܹÕĒö}e<•Ķpg3-žjźvī…–KŗźŃ7`{I,[|Ģ/ÉĀģ¾²š©|zÜ}’]zU°ę@å ÷Æ4ŅėŸ;Öšpč‚•Š£µxFŗøÓ2Œ£¬”ĀśćŌ ¼Fć„öņé_,‘±8īÜsW>PĮ„iÉĒ;UEĮ„Šƒ„tݾ΄&…ÜšĆŌķ?AżĄ«ļĢZNOhKż™Ć”™hæū!nŸ•ŌŒml+Ļz¢:®–żi1ņ0@yV>~ńё“Šyjń¹³Ķ]¢G…¬‡Ųē ?īÄ¢%’ŌŌT|Š74yY–¶‰%ķ™I“ LšńŠŗ G±™ÖĪ`Ō屦eĮČ*« :x}ܶ²Ō{tz#TNńpÆcˆ‚;pw"ĪYO}—ĒYWł4Š“§`Üxʎ§KGNļ!k¶—ī¦><ÉETļÆĆlSX¾Ękä ³ų˜Uł Īō :~šīĀĆś{{¼<ł\•µ9“Eøód-É>n½~‰¶…‹¹~ę’ŽŸUł ;Ó=Üyē"Ķcpŗļ1i{žś½•Š·ų¼ÆńÖFŚŗ‚Ģä99ø­—w›:0„$±½“„ZG¦a“ęz µć;¹ŗō› sł…*īę!qŃZ>-¦‡F˜¦² ųiģ{øź¶J0 _’ė\j˜»Cė~—S’{÷U•?~ü}g2©Ņ{Rč BQŌµģbłźZV×¶ėŖ«kwwŻ¢[uögo,ŗ@DQ@)Az „$$RH#½ĶLęžž¤—{Ć ńóņÉóČĢs?§ÜsΜ¹åŖ#Ā‘‡”Ÿ#Ł«ŲsæzUfīāąÉę/Ÿ•ß­#,ųZ"½[źC+­ķ@k¹˜ÜŲHŁĪķżfń1 ŲźÓ9Pb@=ūe·É’EnY" į@ß­„ĒȬSZŅm+$Ņ„r/G* ķŽ/>Yš4iP3µV3*F¬fP-ØØŖ-XT]U]łh,ü†ŹĘk ˆg% ‘įųŹÉÉÆāÜŻµĘ§UĒ2čŖLZŽ3ŌP–]‰z6–&KU5‰Dxyrīžfö.?G²G;°÷~õP•Z*‹źZźšüŚŌ‡ę“4¶Ķåb±bV]ń @=ŁżQ5·?éiŃxŗ‚š±ž$T9P­3åZ=ÓYńu&KŽ©?ZŁ{L}ÉŚŽ#gRTWB’rMҹ,yŽuø×²0˜P]‰WT2ŌęÅŖŠ`OlE„-ŸĶƬ%fp8Š·ÅGU1S–u¬RK»×•ż 2 eŪŁŗ1Žį£b FPh=)Čo^ŲŅÓNϱGeļž44Ų CEūøj NP£$ąŃf;{Ļ#ś}?é@ƒFFć۔ǪLóß·B!„øųō›ÅGwo0ŌŌµ›d«ØŌZĶ`p^i[;¤Ū–‡7Ø.cłŃĘ¶CQPMÄ»«?{«(„ł„Q@i’EXo>TTN”›™ι/&Į‘Žø•ćH„‘sgi‰/£Į1—nŖXس“¹ō÷lLŖ[ė6ö.?G:ßvŠ×rīiæzµ­sŚÖ‡½i-«ķ(ĒŽG3nč4® ®”¢¢’ŠŹBöĢķÓ~ķ™^SõnźÆÅ×ßÕ6üƒĢ˜?āŻU²‚į§Ō’SnÅQOrÕr9“Ŗ˜)=¾‹ć%Tł„¦ŽŅŠ:MŸµ¹Ŗ ˜ÉT €Š‹1•Ś#mĪ$-«ĆĢ»ŹśVĘõyŁ”’p„¢R{&“=[šĻęņńLTģp¢¢'°@©aŻ®ņNŪ—)¤į²ĮŒr;NĒåN=łÕRĪZāSQ)Č/¤'ēÓßŲ£æ²wźeKiM»ølÖ,źĢ‰ķ9č‰³śIGįEcv¦S>…B!Äŧß,>:’¹—ī­…ŲžYÕéuÅl”ø^Įhźž³c&0¼Oq•dUŃ8=œ‘īG9Ņ``°Æ+¹ł&ū½Å×é\˜³ŹOÆómēŹŁŌϤŠ„Ø*6„šćėŗæ_oūķļ“”‹ŠJĮŽ/(=Īą°P‰ˆ ':,”•ė:?°„7öLOE„¤†ĀX6æśd× ! ÄH¾É“µ”“ ƒÓ#{źķ^5œŹ'æ“ĶӔ5~ŽÓ”sKŸeˆ ĒO†ĻćGńĶÆ)Š #½”¬ļgzŚk¤m:å„9”—ęP—rc‚ÓՙqÖŖ]T_GŌ0’N‹z󫄜µÄ7lĢhL݇ŖXØĪ=ŹÉʾ5h{õWŽ·śŪ¢˜3śIGń‰½¤ł2÷cÕ\T£B!„pš~³ųŲP6_ĻNÆ{¹ø‚Åq—&ÖWÉU„¤ø¤ė z›wwx_O>šNåSِHx¤Ēk’ä~†¼ģÖK®ķŸƒŁ»üœEO>,† ҷאsößŖb” Ŗo_¦ gŸŠńŖs×ĪŪ:ƒ®śUĄ\™OFe>€GŠ4fΌ`fŌ.¾ÉķĆĆFģ˜^i¹…QA„Dłb)ŽAn¹ƒƒ Ąƒ¦ņ’~ÓķÅŠxt½źÆ˜»9ėøe8šŹJ[žä łŚ—KķŁōՕać‰ns–wæq6ĘņZ us*ŖyYÄĒGÜįŹń>åWO9wŸ8ŽŲø¶÷|ģģLu'+ūž€ŸīhķÆģ=nÕZĄä5€¶—²]bńģČ÷baļ~҆ĘųŅ”æ•Œ:ē’ø)„B!.Ž}‡„ÅąNB@ėS.±DłC}IĻ÷·:Å4&!ĄŲ<Éīų§“ž|XŒ™”›ńĖŠČ@\ėŠŁ_پJģ_˾-PMşߗGŇ £^Ē5×Ķ#ŃŪń_ČtåCŅüņĻžäõ}ńŹÜTƒUõb_ėļīžSźī“&;ė­č®ß6ļYŖkšÜ⮿zÓėMMQ%fÆp†»P}Ŗ’śāźć‰ös„¶¼ė§`kaÆćČŽŹėkQ=};Å8¬u•Ż|JŸŠ±ó tm¤4§ åµW¬ÅŁd䐟×ü——‚ćEuŲ|ƒœ^NŽ]¾bB©é~qŖ:ē$Å.D…“_еw~µÄ§–ģa͊¬čįos‡‡ćŲ«jķÆģ=.Õbó oæGx8:t“ŽšGōū~R‡č)“™8uX·ļ»‡L'Ģ»†“šźŖ·ōµB!„ų~é7g>Vd~EaĢuÄN˜‰kz!Åx5l0>†"ŅĮQ?æW„}Åńą+‰:™”•+Ų|½ @€KŸ›Ó{"}͇Ey•˜Ē !ĪŻDCŽÉNŁ“w|ēŲN—SÆĘ3iĒ*±(PXŲżS_»ćØųPĄĶŖbÅzμpX>za+ŽM±ł:§0Ī»€ c Q‘ƒ°š”‡«Õķ·’^ځÖr1LfĪ”ATPRQÅśaQųZ‹ŁŸßDdžŻŪ~õ¦×ky95.±ŹgE™8B‰ķ:ā4RT\ “^ ą€÷@T#żü(6(ØJe%õŗņį,åĒ©ŒH`ųģ Ģ/ ²Ńæč(ĀÜŖ)8Ńóż»åʘa®`ņ`(AžŌme[AóBLk#•%„Ŗ§²“«-”°2»yjtOĘ'2¼ÜŚī5U©åŲŃΉé‰OüLfO.ćLA % ˜½¼ %h@#e»wū¹&k&¹gFźŁš‘ß¾Ę×{µS­ż•ŻĒÕCĒ)‰H`Ä“)ødPēĮŠĪŪ9hŃ_ūɾ Œ ĢŻDœ!L[ēņˆŒ”dū5ފ¢·ōµB!„ų~é7‹;7īaŌäJ$Z1S_UHZźAŽÕ9vzpćjźĘĻ$:l$ańŠTc„®Ŗ„Œģ¾=eWO>źó 93.€`÷NeWŅÕÉØöŽĄŚp€½{0fxć§6_3Wqōs6mŌ–#ā3ŗÄā>@ÅZuā‚ŻĻłŠb’Ī,\“"‰åC@U'·Ä+e*įŻk3-ķ@K¹4Uļ%Æz&aaqĹāf1S_U@ZźA²k;W^oū՛ž–|VXāXÕz–^y”5¾‚ŅÓ­Ē\ąˆyLqīĮ ŠāÉšY lj6«Vī×]~Ī`«É`ŪVHCDŌX†(fźźKÉH=HZ¾µ÷ŗāLÜØ KsZŁū޲?§õ,Jæˆ`\”ZJOw±ˆR¼› óuų†Ą }O½VTW|BćŚ!ĘbŻ‹eé‡ČB`šb7·«ŗśRN¤e_~}Ÿ-I/ĆҲČęˆüžO|=±g;ÕŚ_ٳ?m²d°k‹‰¤¤b&b(/%gG:u3ĘćßįįeŽ˜Gō×~ņ|tuŁ‹qą8¢)Ūž£{ĢÕz½·B!„ßJ\\|’ŗk»€1`—ĶHÉöÆų®Æ‹%B!.ZÓ®»£kų*͹‹ż‹ˆ”ELō9ĮŠ5‡œŠB!„øČō«3…8Ē'Ā„2G…Btāī7_„–āÓu“½m‚č;cé)g¤;; !„Bq’3…BŃÆ%Ī™†±¤ ł¾«īDމǯz?«7÷ż^˜B!„Bˆ CĪ|Bń½5|źlBL6¬MMŻnc3”³żŪ“ •°·ņJāĀFē ÖZŖŠ°ś;YxB!„āū@Ī|B!„B!„B8„ !„B!„B!B…B!„B!„!‹B!„B!„B‡ÅG!„B!„B!„CČā£B!„B!„Ā!dńQ!„B!„Bį²ų(„B!„B!„pY|B!„B!„B8„,> !„B!„B!B…B!„B!„!‹B!„B!„B‡ÅG!„B!„B!„CČā£B!„B!„Ā!dńQ!„B!„Bį²ų(„B!„B!„pY|B!„B!„B8„,> !„B!„B!B…B!„B!„!‹B!„B!„B‡ÅG!„B!„B!„Cō›ÅGOŪ\żßržvkˆ³C¢_uÓæxļż_0 ›³C¹ ~hł½ĀS~Ļ»?Ć>Mš?će^ČS¾Ļ‹Wy902!„ēøŗŽē¦'^ęµ·–³|ły’¹g‡$„B”‹‹³h”* ((ŠźģHÄŲ€˜…,œRŇ˶ößż* Š (Ķł4äבõę֐Čmož† ĒžĄ/¶{śēė¼āSEA”÷»Ī:./&ĆHę’ā.‚‡{-„G·óį+ļ²³Ģč°ō|‡_ɍ×NfXX¾&Ģu%ŻĘŹ·W°³¬żBwčØ[øå†ÉĆū`j*'?g3ŸžæĪŪ ĪiĻżĮø»īåņøB¶żļ#öVZ”ź˜Ć÷iļ~÷béĒ/–|!„Zæ9óQˆžĄ'a&‹~4Ł¦ {–žż¦-»Ÿ›ox‰ŹćšÕ’_G֛ĻĢ«™”ĒÖĻöŚ=m{øŠńÕŗ®åłߥ£ŸÖ^żóŽĖ‹Į¼§ƍćÜÉŻł+æČFr>£ūųCŸ–ō|†Oc“-'ŅSŁøv[÷•į™p žé~¦ l­Ć€Ø{xō™ŒōĢ`ĖŹwųp}:Źąkxšł›H’"EœŃž§ßłg–¾ū6ļ¼ņY8ŲN9ŃĪ­r$#GyPō–¬ŪBjj*©ig¾_{÷»K?~±äC!„øŠśĻ™Bō#ĪśŽ+ß·Ļ#Źoś5#pÉ|·²]ퟸō÷ųģIŽ}ü湏‘6rŽż=ś¼ ųœ‡~ĶoŸšĆā+xꋇ¤—³ź1īYÕž³Ak~ɳ/LāŹK¼Ł¶Ŗy»17N!Øao>ów¾®1ėIŻŽČÓ’˜Ć• V²ļ‹źó/qŃpF{޽ņļܑœĖG/æMūl®¹żQīĖż9K_ø’£.®`­»p?ų“ŪæūŻ‹„æXņ!„B\(dńŃÓo‹ļ]Äųø0|©”äŌ>¾|ū Öd5öų9·AsyąłŪˆ­_Įæžż˜“śĀ5¦F.ꧤ0<ĀkĮQ¾}{/ƒ¹a[Ÿį—Æg·l××rīNŪōüh¤¦*ŸĢŒµ¼šÆm-ŪĶxžæ’Ö’īKL‹®cj\.¶|¼„æ®=ѲŃc$—.¾’ÉIŃDxĀK©ķ2¾°ŁāO÷Ō±łŸ9^5a‘.XK³ŲõžR^O­ŌŸn ‰Üśī£Ģš“a0QĢ‘ÜłīūÜnkžUūŌēšųūķņ2ļęX0)’ĄPYÉ¾^åÕŌ]ńéŁÆū™Dn\ž(s|šßkRv²ō¦—ł®ĆuČZė“·+­ō“—•wü†O*›»·ŠQÜüžSLĶłw¼xXs~UomłF>ĄĢ!ÕģqSŸņ ąY?‹ū?»›ŠUńļ¬ĖY|ó8ā‚]PŹKŲ÷Ī3ü3µ¾u#®ā¦Å39ÄO,TŸÅ`Ąµisļpē›w’Įó9F†^~7żź9”²y-CŃ_oݰęŸį;“JŠ”;¹un%’ų!{šP,Ō“žMa2ŽäĘß>ƼĄ<¾ŪškN»<—i>‹·śŻŃ 9>=ūmō;Čŗ„aI%tž=ܜd@QUP:/ČkŖķJ+{ևÖü:¢Ž:{ÓüĪ|Į»Ūß®/łõ}7M­c’Ś7ų²Ä‚Oō,Fų{Ķ‹Į£ę駒ń.ÜĻ–7cu%4n Óē%°|×·ŗāÓÓ-C˜žĖ Šö~ĮŠ-ŽD̘ĆģūžÄ­ō×,M³¶lw`åųĖ&\™ĘµO¤`čā· =åÜ[~õŌÆ½ŪßÅ""ŹCeėėZļ_×0`'L”Cmł–Ć&ķ?źéMĻk`&^”q\rķl¢Øa’”“Ø .®€bé“«YĮ1h^|tfżj9.µou›ŁwävĘ'„ķGO‹Ę’qĖĀc_ś«žś­ćÆÖqAo|žåÓømĶżLöL磛Ÿau}ßī§čŒö\|ŖŠą‰sˆ’zE޳˜2ĘFŁ[Å-Ū»śĘā’In‰öżjķŸ+÷æÅžāĶ€ŚŃ\ņĢÄe,ćåĻņ°TŸŠ¼æ–Xķ#aüm$NŽ‚œę³jN’‡nkæķėŹxą›¹dĮV½vŖŻ{y›žĘkߔPšņ›DŠY7Mēµg·ö)¾Ž9@!>“U©åÄĮ“vž F-¼“KĆO±ī7ϲģŌŁūżl\Ķ©gßį–Å—ĆŽOtŧuæ*j˶Ö1VHź9?½Õ‡Öv„•½ėCk~ķ]om š¾K“mœxg]ņ; ŚĢüŽ5…g»Ż]{ųźģ{nõ \~ß8ü‹ÖšĒG—sŲzī‹ģg,릌zŠOO Ø®4ų=Ļ,m^qū" Ć+æbāM3XśÄʖķŖNįąIš2‡S£LĒ»‹˜“–³Öüj­_{·æ‹EĄ@Pj‹‰l eųĻqMšF–<ō µl>¾ųõ=µJoz3}™›‡Śšld­ }żRž’ŚÜ.Ģ(/T°Ę3ZIå°Śü9c@4‘”*jƒcQŁā“śÕŚNµoG7§įį$nś:ļž½M‚§ń2ʏV)ś“õŒØ>õW=ō/ZĒ_­ćB_ā³g“ēŌO^gćÓ÷óܛ7£j8µõU–&qĘ\fĻNfš ö浔·K“_­µ½XŖO~Ük\I¶€µ"ƒrt•Y[öž—hmWާ³7¼ÉšÆU*ĒNåī‘ūYłęz²˜4cƒ#­pĢØ»ö6o’ńH!„8?’ł-6) Cé>^IWPĻžWWż)[³¼‡'wž€G2×’įĢńÜĻ›OæÅžŹīĀ#Ąˆ$Œ¹ŪXqŚ„„üJ6¦’Ūį“#ŻåÜ CE5Ŗ~I£ZŅkū_[6c Ēw—µ¼×TVLI-ų¶lÓe59–+ ģ^1é©§[¶3›Ņ8’V‡iH2UUw|Ztž\×é%„„į’»™e§lķŽĻŲVˆ4ŒyF›®ų“ī·ėm»¦„>“¶+­ģ] -æö®·¶/žITĆNÖ|]Łé½¾ä·)żsÖŗt¹­Ėˆi$ł›9öł [MéõŸžžĄf,ēŲ†“–ķ »Ų½Æ—!I¤4un«=ՈÖr֚_­õėˆö÷}ēV9W,l5 ÷f`H įž*M `s3įŖ£hś’Ž”’.aéæ_ć­UߑQZIeQmK}ØØģü6‹FæŁüōž©Œ õ'pš®~rŃõ(Ŗ+īźļµŅŚNµoU©©dŌ‡8=²å5ĻijČcļ·'[^ėKÕS’Ņe™u1žjōĘWļ»c»÷°gēQNŁś6Ę8«=7Õąż'~Ę]ü‘%Æ­%Ćėjžć}Ģ^ÄöwŸäWż•wÕ鬋ÖöŅU ?Ÿvoļy‰Öv„5=­ķJ5ŌPYnAE„¾ŚŠįL-‡L jq9µŖ‰A\t„śę±½åCĘ#!„¢k?óŃ×”мN–Ņ2 Ä3QUŁŁęRŹŠé7¢(ŠNl€Ā–źī@ķ^3Ž *u9EķŹĻVVB…üŚl«§œ|0ŚĪ~1QĢ”•VµŪwe9_|3™»S~Ć{o•q2ė8's°ķ‹-)m‰'J#•U­ėŲŖKóetj‡o#ÜĒē!*Č WUm¾oŽĮ†±¤żƒ2lĘrJĖŚ¤‡Jՙ:l^¾„xŚ ŽØ+>-łÕ½fž*Ŗēm¼ūŽ-ķŽ3]P]- °A‘A_łŁ[/õ”§]ŚĖ¦§ŽĪńlšĖÓQ¹q ;:Ÿ©Š—üVēuūÅĆ=8?„–ù•Øøõš§ŽāÓÓ؆r Ź]ŚmWVT &_‚żš RŪ0”§œõę·7ż¹ż9‹yŠQ¬fL& ķfŁo_`‡G6ėl¤øƒ”тYĒ:P_ŅĖOßN>@ź·ģŽ| żū Ƙ?ml~ĢÉ/ŸęĶøæqĖōūyjšŠM±Pvt9kw-ęŗ¤ĢgŪØ³źWk;Õz¼Õ+Ųyōf~–”ļ4Ÿu5zj,łŸ±¶R:IDATģ” ö©æ‚žūč}üÕ:.ō%>•Æ_ś_Ķ÷VŌ?ÆsV{vqĆ„·/bFb ™»Ł²že6Ö^ĀĶ÷_Ͻ“p"u9Ͻ¾[W^ōĪ‹ķÅĒ‘Öy]o4·«sW¼74—@©iS–&pķĆq¤q«…ŒGB!Dלž“k.“)ßĶ[/ģ"īńū˜ń‹Ål}ų}2ūųKł÷ŸżNLm[Ī^ęÜūņ͌<»˜d3³į‰ū[.ƒę‰pź’8¶>…ÉćĒ0|Ħ.œĄĢ”q¼žŠ‹l®ÕwϦ!‹^䑛|(żv5Ė>Č£øÖ‚ŃČ{ļfz‡mµó Mīķ’­'>-łÕ¦¹>źŅžĖßWžģü¶„Ž“Ó. ŲæüģK_»rF{±/ķõvNčÕ ć’Ī’>ĶFķ¢›ģK~Ķ–Ī÷¶ė«ŽāėM§~· j÷āė-UŠWĪöҿ۟sØØ”Vƒź„ŠJķÉżPĄĒĖ„”¢œ «»Į¾¤×vq„¢čŸøŒĖēN€[Žßņļ‡ŲõŽP†õĘPz‚Ƨ*˜’ģ(5•ģU•ļAŚ»sĒ›ŠŹ‘MŁŌżz,7¾Ļʢ˘<ĀÄéĶ[Ś”Uߎ£žśmćÆÖq”ońļ_Īj϶¦zŹ­ąń’¤Ķ?üÜ’ĪTŒ_½ĪĖGBøō§÷ó§+Åć«ĖĻ+miéŸūĀ™óŗŽikWzh¦ŖŃ‚™s%'ć‘BŃß8|ń±¼ ŌŠšNÆūū› ¾˜&¼…»ßgĆÉÓģy<#Ÿ›ĻŻ’·•GŽÉst˜żRƒ×.Jj<‚ƒŚ½nš Ą§Ć[O9ט¾ą“%Ul>ū]@5ŌpŗŠŌ9ĪdmemÖVÖa‰ņäÉ̚äÉꍶī 8yZ¦“—xlIjĖkõžLØBū‡ź¢Ų| ±¶;ć*pŠ 5™×)­“EńiĪo®jē/J ^»(+Sšōj$ķpZ×TŚ’æŽņėjæö¦§]}Ū‹ŅhLxŗµęÓØśą>Ą>y³G½¹Ö'pée”4ī~›ÕgzųrbĒć£”Ø„ruaŽp¬ēĻj‰OO Ų| ó±B›‡ŠųB1Ÿ øÜ yMBO9ėÉo[=v¬‹E^n¶”\źŁÄ†³ÕpÆGX“Š%?‡C:ĪqŽé)ąāøłuz½”2›#ūš’én˜ĪČ8•Ŗ½‡Ļ»?=_ZŪ©žć­zĖ.²ī½‰Ä)!lĢH¬W©_årnÕL÷8£–ńWėøąˆų“rJ{¶dņŻZņä>n<ĆĖ×pŪæ ×=‰æ^7VÆ×¼_½ób»²ć¼Dϼ®·ōģŻ®.D;•ńH!„ŠĒį÷|<~°5 ‰;ć[i‹H‰U©ĪŲ×yšWUP äČ?yk{#asąÖĮŽŽ²ŸRąHz¶či\īÓśŌŁAÓĘek?éŃUĪ ¤oŻĘ¶mĶŪ· »¾›Y˜ŅśW]Lµ¢`r׿@ęźAKŻžū4u.É^Ó24ł?7±åßīę)$'¹`=¹æó¤\K|:ņk.³ Ø^xt‘Göģ)¤)fwʛŚķuńå×ć~ķMG»:·½½ŚKCEµ¶`¢†¶^Āč‘2…ÓłåŪžõę;ój&±uõžŽæœŲéų°¦§räŒQ‹~Dœ”ēĻk‰OOŠ|¼hł·kÓ8ʍõĄ–™¦ļ©œ:ŹYO~AĒńa§śøXnJ§ĪǬ­ J>g“< ‘¼ļöwŁ~ę?ų?tcŻ:ßWPkz±!>‚mŌēwńĄŒ6õ6ś§×3νˆ#kÓ{ÜīBŌÆÖvŖēx«ułœ½Ē­DM›ĒģYĆń,ŻĒ¹mV÷ś2ĪōBÓų«u\čc|=µ+­œŅžĻż£((“–„RƂĪK¤uĻ‹ķĶNó=óŗ^Ó³w»wĄq¤)bńH!„håš3óÖ¬`×¼{™ó«g0¬N%ĖĪä³b<ĘŹŅh¾’OH[ś_v¾|óń¶?ņĮņņėćĖ֑1åĒüų©‡ųŁŠż'°`†?–EŃērī†wōĻųķ#įģ:́S…4)ƒI^4‹š¦4ž»£^wzT2ŽOyę.¶d6ā6‰9ӂi¬ī\§Ŗ”Ϥxö®5lÉöfų̹LTÄw/mo™”Ū;¾sjӏSŅ4—¹w’„ŠõĒ©°(ŌēäčŁūō|ō_Ox¹OżßM8˜Ó„1,ŠŲ! ŒõZĖķO®ļS|½ķ×3h1Į͟ uA±ł3"ZØM%>R¤+ŸZŪ•VZók>¼‡“źÉL¾õIīŒJå“2ŠÉS#h¬VŚuFzókÆzH¹v.ĒßąĶ,S·_ķŻžŻ÷±ź?ūõų"~żbßl:Ha'!£'3ią—üņĻ[uŧ§?P3žIóĢ]kŲzĢ@ĢÜ«I ,ā»W¾Ńż%Xk9ėÉ/ō^æŽź¾ļŠ3_äÓģw¹ń†'yŌļKŽŌaś‚džYĻ’/ŖčXĮåS‰IN&ŃÓ“Ģ’ØģļCzn#˜sßSLā'2 )©hĄä;œ1)1ų6å°nåŽÖżŁ.įg’šŒr(—ŅWü†'09>€3©/šņ±Öż:«~µ¶S]ćÆi›NŃxĻ|™(’fk§ćLO„…ÖńWėø 7¾ŽŚ•VĪhĻÕŻĶ1æ«xģŠBÖd†0ē¶!ķ~UW>ģ=_ÓŹŽó=ó:-飻ŻŪ;=­łńH!„čšŃßß’9GīĄjĖåšwåx O`Ņ“& ÅX¶ŸĻ^ž«N“Žx٤Ę0ń'I Č\Ēśżµ-ŸĶ=ƄĖf’č±Ÿ/Śļž:ßs:Ēöy1a"“fc¤o9{^ŁDEJ2§Ö±vOóƒ?“–³VMŖ ŽC‡—ČÄ©S™0:—3ß±ę„Wų<Æ5=Æč9̟髿āhcóYR. A$\7ƒČŠ-¬ŽÖ|wšÓūÖP2™ä¤™¤$Ē2 —Æ|BéˆY kÜĮĒß6_Zļ=ōR.™TĪę7£Ģ^Ä„3G¤œ`Ēk/±tw•īųt—wķ~ņėbˆKJaö̦NŹ`ŪF¾>Ōpv慤}s„Z’Ę$OgZŹhbB|0VeÓśĶ¤ēÕö)¾Žö{õ3nū‰ŁÆ.a[a÷gčč©_“u0oĻĄōõ¬?TÓmšÕEŪŲTÅ?>™I“S˜”4_S»>ߥ”ü]ńiķ¼£ēpÉ“3l~q3¶i‹˜i”œ=ŽvuŻ\›ā˜²8·C«Łp¬ż}ę“–³ÖüžÓ{ż:¦?øœŲ‘IcųHĘM›Ī„ŽŌęlāķæ~ĄžŚĪķǵ!Ф›&éZʑ›8fķ|ękoé5¹—bµ…NtÜpbnj Źßș̭|śŸ„¬Čhm3³ž£Ē“œ4†± Qų)§Ł÷ł«<ūęĮvūtfżji§zĒ_k#®M&ȵ˜Żo¼Ēīņö‹zŽ#-ż‹ÖńWėø '>ŠÖ®“ŗŠķ¹#«õ89Ēƙtćõ\=g0źį÷ųĶėGtåAo{q1‡3öś)„žŽČš}ŸŪ{^¢µ]iMOK»rU†3ķśx¾YĶŽb…qW‘āŸĘ'_fż‘€’ŗ‚myĶķTė¼I{>d<B!ŗ¢ÄÅÅĖ5ß3k/åžĻoĒ’Ó{yģæׂlų%/šē{ėųč’žeuĆļ Śø˜Ū•V³{‡»†¬ēƟ/wv(]źńyŗ,ä”å’‡ĒŪ?ćɵŻ/¦ !¾’d\B!„ö «;ßC ‰ UΐwØĢŁ”8ĪxšŠhļŃ®ząbÅrj#Ė_]ćģPŗŌ_ās ˆ&Dm¤²¼Ā©q!ļ‡>.!„Būpų=ÅłqÆĒKąz`‡²Ī`šL`Ö Éx_ĘæŹŚ±čiWY]²Ų¶<ĖŁatĖ™ńy…'3v°ŠŃ‡„…“ńmHcåįŽ=x…ø˜Éø „B!Eū¹Æ=䜺šĖ¦ż˜‰—y¢Ō—rźČūüżÕµĪM|I»zųNøž{Ə ¶"Æ–.åėj¹i¾„B!„£Č=…B!„B!„!×Ń!„B!„B!B…B!„B!„!‹B\ īµć¹÷Żåüėg1ĪE!„B!„ā‚ųĮ?pęĘ?`Q¤­åßęŚ ²w²iŁū|™ŪŲņśŸ’€«ż×ņ§;ŽįkėS^/žn ŻĄßn~½n ·¾ōóƒČłą~ž\UӲݸŽäWSšļÅ’`‡āœ5ß w½ÉC³Ż{ŽčÄÜšų׋a@ĢBN©āĆe[{ŽN¹Š’·˜ńķÓ÷šŹ17‡ÅtĮØ Š¢ (­·Yuf{ńŽXČ ·^Bb|¾ŌPQ]ĢÉ£ŪY÷æĻŲ_*æK!„B!„āüżąŌ†clžd/E `Ph,ćRęsė£ńzäiVžÖ_DŠźŹŁ `ÕĒˆ¶ļ v®āÓ*O›1³ę2Ņ#›ļ¾>D‘µy„0Ē”1ų$Ģdя*(łß·l²tæĄå5q,1ę"ĪT3lJ+rh\ĪäŒöāx+’aĮĒŲ·įS²­&"B∟|5óv¬fé E!„B!„±’oŽŌˆÕ½ĪčIEND®B`‚go-snaps-0.5.16/images/summary-removed.png000066400000000000000000001142351511036776100204560ustar00rootroot00000000000000‰PNG  IHDR"H†ˆ’sBIT|dˆtEXtSoftwaregnome-screenshotļæ>*tEXtCreation TimeFri 30 Sep 2022 22:18:29 BSTür ~ IDATxœģŻw˜TÕżĒń÷Ł^`é½³€€R„ƒˆ {7¶{ļ±&š`‰±Åńcģb¢±W,`ÅŅ–Ž{Ż…ķ÷÷ĒąĀ ź ī‰ļĻ>°3ēžū=ē’łxī½AĖ–-C€ōō $I’$I’$)";»I’$I’$I’ū "%I’$I’$%A¤$I’$I’¤¤3ˆ”$I’$I’”t);ņdśžž»/ķXõ{”R“v!3'¼ĘæūŁƒjķs‚ùōßdzKy’śóųū“ō­ś¬Õü NųÕ>tėŠ”:²fż2ęNĖ’~™/WDn'I’$I’$©ęķŠ bįć¼Ÿfģ¬(aV­; ×Š ųM› Īŗęjm³ūt§]éRV­iDĒž-aŚŅjßē5ųמa8J¦ńÅ[/2«<•ęóéŠļö’ų%¾\‘X;I’$I’$IɱƃH‚R–¾ż*/Mśv÷ć㠾⟜Ū÷Īnż:ĢI«jŚaP{¢3å•'rj×ĄÕŗŚż—ĆhĪŗü6Ž)ܼ«1šÖ†ĘY‘„ŪI’$I’$IJŽ”Ā…Õ~›öéb‚Š:4l¼łÖ쌢ŽtļœĀ’ ųź³llŚ—C3*Ŗ×¢q:‘EŖ…‹„³Yø&Hø$I’$I’¤äŲ%¶Ön™A)Eå›ĖÉč҇üŚK™õõbÖMü’™©mčÖ'µŚq+ ”²iOö®SńŻ.·«$I’$I’¤äŲ)Adj݆ԯWŸśŪÓwÆĖ8sßz„ėæäćÆ7ļNl6 3u7LcüģT6–æĪģŁ“éŪ½Z?_??5éŻ9óīæ0ā²S8dŸīä׊nu¾xŪI’$I’$IJŽhķŚµG¤¤$’q‘9͇r`ߦ4ī}Ć΁ „_ļfd­ū’ē·—l¾e{ß_žBūE/r’ū‹ ©l¾CūFYšÜx±Ą²hŁX&|SA­fķČļ¶;½z dÆC÷„G³uŒłdnU_ń¶“$I’$I’”;å­Ł³FŻĻkÓ£PV̕‹˜2+öŚźpÓ³#³²c·V!‹_\H½zõ(šµˆŅwgĻN%|:-£Ŗż’)/ņ×/Ц˾ :ų0†õ;‡»Ėpéßf%ŌN’$I’$IRrģ”·fÆüücĘnq6ßy_LNĻ=i_Ā!·š—ƒæm©Ų­o{˜¶ ŖmøÅ‹ofMz‹Y“Žb͈ń‹žCéQ2ƒ/Ņ# µ“$I’$I’Tóv| †|’ŪŖ;ōn …Ÿņä#²¼,Ö.ØĢ”DZē0“k_`Į¶ÜŌåÜÅ;咓ŗķfq·“$I’$I’T#v¹m€„żéŽ%…āÉ£yõĆĻųō“ń|śÉx>?†÷>[AYŪ.œW@³Żwßźų”ČnģŁ9‹č²e|Dj'I’$I’$)9vĪŽČŽuOvĻ-aĮÄé[ݲ½xā 6Ӎ={fóźč: ’ ·œ?›©_OaęܕÖnB÷ރčŅd’xµźųxŪI’$I’$IJŽ].ˆl: 3ŁŃ…Ģ˜øØ~Ļté—_1§¤/t†Ń_0å…'yūŠ>ģ±ū`: Ź"­l+—Lå­{žā‘ÖUo;I’$I’$IÉ“lŁ2HOĻŲŁµH’$I’$IśåĆ%I’$I’$%A¤$I’$I’¤¤3ˆ”$I’$I’”t‘’$I’$I’’Ī R’$I’$IRŅDJ’$I’$IJŗ”]€“3¤„õęŲ+Nah~}²ÓC‚é÷sˆwvY’$I’$I’³ "ęR#9ąāS8øKc23ŠX1e,O’ż1>]Ż%śK–^gŸĻĮł‹łčßĻ0am9¬›–ōsę“;ˆƒśÆćé'j&š¬éžv–’•qH’$I’¤ę­Ł?sū_ 'öŹ`Ž§Ļšüė³ŪĀ„7ŸF× LZƒĻ¼•æ=öžż\qPėIüŅ×v¦s—L6Nł÷½ńćʍcÜ7«’~޼=öāŠ#³wjå.ŁßĪņæ2I’$I’ōĆ "ĘźuøˆĆ:W2ū_7ńǼʋ#’Ä]÷L °ī0Žž›”žŚvgō\Ā«÷ÜŹŻ/ ö1WqA×@ŃlRŅ |CŃ=oÕł·/ćŻażķ,’+ć$I’$IŪ¶SnĶ®³Ūįœtü:·©Ge¬Ÿ;™q/=ĢČńkŖŚdÕŹńēJļü¦Ō”åóæąĶGžÉ+3Jbßoʹ’9‡¶ü– ˜U­’fūÜĘmē•1źü«y|Y*ŃĢĪģņ ļŪ‚9°vQ_<óŒ[^ķŲ¬C¹šåshņĀÕüuĘĮr/ņ„¬^ĪžŽ»Ēm$šŁ™żŽ?Œ~=ZѼVm²ƒ¢­źūVƒĒsźEƒčŌ<“ņESx’‘ Ō¾āt:~ų;.ypsŻńÖW“šķAvÅtž³ ˆŻ:½zÜ»|^Ų“ż{ĄėŌx-ŗŌgį«×ņźg%ĄT¢Żā¤=šĀ¤%?i,?¶^ ¾‘?ŸÓ†H$B„0š&žč Aƒ‚8é†ÄĘūŻóÖ„„Āu )˜ž·ŻūéÅŻųÕcW1$«’H$JPŚ‚3{ŠÓ+cē’źE\ūTlŻĒ³®éļŪ>h]å‡såӇ¾ł©żö¤yń$FŽõ!MO?“½Z³ąõ?sŻČq÷ŠpČĶÜu1ŒŗńMR=†łõI©\Ȥ’ÜĒÆĶŁ®qüŲ)æž/,MaCę»|1łtz÷T"»lEdįŖBČŌhgN¼įjöo°€OŽśÆ,I§UĻ}xéļ©^ĮoUgV×sø|Ą¾|퟼¹¼Œ¼VCŁ­^6°‘¬ŚC9dH=–Mų”Ń3ē±<ڌ®ƒ÷įųMH¹öZ^˜›Ö¬ģĆ8÷ęĆh_ųcŸĄņ¼>pŽńdfD(Łb/j¢õe­ČiÆ\H欩øžž>5Öfģ{+¹ö¾_pĄ yįīéL3“ā_÷ą”¶ņŲ¬4²¢Ņ»kČŅ7ļ”jyŠ™ģ×l>o\ó{ž˜æé6ąŃ/1’÷rŹńĆĒĻnUkN«Ržuō¼²xӍ’œ·7}Wøä~.:­zū±o¬ä¢žĢ>ĆŪšĀ?ęŠžŲƒč”2ēnŗ•ē–¤£™¼üV~F[ī›ÜžśjBż\Š–Ń¢¢ ~3‚£ę¾ĖŸeMQ•yuØ ųĮxś÷샌¾žBFĄ}“ŃmȾģ½wOZ×^ė’G–ĒŪt¼ė„lż¦~…iō,ƒņ5Słś«Ł ĶŁ–R›ö¦sÓ ßūö¾ā7ąa ˆ±ĻOžŠÅ@³¶ƒ"ę|ż_—ožÅ»®āķ/‘u5ė­‡x坐µŻpNē/yž”QĢjPŸ¾CŅŗE9L‹&¼N—¾‹| žåeöč}Żśµ„ŁKG¼ó,I’$I’vm;t+QŹnéQƔiÆ>Ǥņ€š;¾Õ¾GS"+¾ąļS7·Ł°žE>œP«SĻŖvėʍcśĘĘtܢ곬{Ņ6²€ ļĻ­ś¬Ē ¦¤Ģ{—'ęWV;ßō6ģȞѭŸQX1õU^Yœ²ĶæūYH…³Y¼: vćU}ģÖ#č¼xnÉę~–ǼHõp/Ńś6ÖĖ“Ļ>ēóO§0æ2ž pKék;“’BY•…ĶiŪ”¹ŪŃ,+¤¢*ÓSIK ߉·æŠ_ńŌoĪāģ+nį¾¼Ęōģ#ųõ-°w§„Œ}ģ·\vł<:qCBc‰w½l½ŅŖ_ĖDEÖTP¦S·G—mƉļœcŪgŻŗŻ­«xū‹w]…‘BÖ®.#$dćśr"«Š˜˜.[MQ˜Jķœ”„ś،.gęg+«ŚT¬\Ęņ"Čk”ų8āgI’$I’“kŪ”;"35¤nPĤyk I’ŽvuźA°fĮV!͊•eŠ®}Oƒ€Į[|:ådĪź1ķĘź: =™ _ꉳé@HFa/źÕ ³Nć±ĒO©vžH4…0­Œœś•°“z&»~ŽŅ 9v~Ēķæ;-f“†±ēÜE*‰.ķĢĢ(ģEܐ ³«÷S¹r9kJ”ī·s²õ…„¼ó—?ń{cāaLiķ)”—šŹ¢ŚŸńÄ ·ńqę,¾ŽPÉ  ˆ””Qš@Ęo)黳ßé‡2¤[J >ćƒQ÷0ŗhN¾šXĪļ;œ9ćF2āĮĻK¼ė„¦–äõ÷śqĪ kxüį•Ģ1“¹ó¾ā£×?`ņŠŠķźóĒÖU¼ā^Wߎ_›ŸR€ p‹¹L…“ķųļ((aķŗ-ÖlJYģļDŅķM’1Ļ’$I’$iĒŪ)/«ł©"Än ™{ćŽžĒ· ZqöxV® ČŹ.į›Ißlū„ †'ż¶$õ›æpõ}ćŖ>ĖܘE’ÜVPuŽå…™V;6R·>y[„{ÉØ/^ ęm ²msöĖŖą­M/˜É(ģEÓV!e g31ÕÄŻ_YŸ|@՘2zõ¦ÓźW8ķÉO˜—Ńƒ;é/Šū¼‰¬—ĄŖņڌy hŚķ*~ū›ž ķ›Å»£·½ĘÓĀm‡Čń¬«xū«éuµ#Öé÷ĶĖ·}':Ļ’$I’$iײCŸY>u“W„ÓņŠ#ɏ|č0óėńõ{pf‡Ķm2sePūõÓæØx„¼Ź„™å“ø?{ķD֊/ųē¼-’¾>’|1ķöēĢ©±c·ųÉmX+įq¤eaX­ŸŚö„göc `ņŌ5T¶ČĮyåU×Ų›–•ÕŪmO}\zæ¾üŗ§oż|Ėx-3• Ń|†Ł–ęõŁ›ž9%,ųäĖmK?tŽøśūöē[A@Ąę¹ 6†ąmŌ‰¬—¤Ųb\ėg-c}š±õś.]YFf“UŪk?®uo5½ī“šßQ\ćųN ń̳$I’$IŚ5ķŠ‘%_šĀż_ŠåŚC¹ņö†¼7ękfŃøk?śę¾É%·~Ą‚Wžcüžē3ģ²ßyi3*›ŃoųPŚD§ńüæ¾!ö\ÄMųfĢ|JĪ;€į SYżŽ‡[O‹žyœwö¼”}Æ»•:cŽāėŁD›¶¤}›=螿§’6žŻw_}µ–ö?•ߝĒ%d7ķ˰(Y_żÄ3Ÿxƒéżćøė.'÷åĻYVoO†©GŁO¬/sõŚõģI·¬, īł2”ź7[Vp;/ĪzŒOų-WÕ}“ÉŪ0xxOrWā¾××ń݉ü±ó&ŚĄ†)Ÿ1­īį\}Čb^)hĢ°ÓŚ°ō³GBė„Õju7\ьEć'ńÕüÅT­éyčPšU|ƓoÜź¼ESg²¼b_ö=ē¬5“5eē}Ķ”MĻ9Œw]ÅŪ_MÆūšī/Žq$:Ļ’$I’$i×­]»ö€””“I®_ś_N ©×”'}ū ¢o¶ŌI]ĄųWßbāĀbŹ+ē1é“Õdvڃ¾Ńw÷&DW~ÉĖ÷ÜĖ s¶~9Eł¢Lv;ŗ' ӖńŁ?ē³ÕՃ‰Šp1ß¼7™¢zķŲ½ē`źJ»ĘyD×NaĢØw™ŗ ØŖmjykśœŲ›Ü©£5±p›cXņÅ+¬n܏ž=öbPĻ4Ī™Ēčæ?ĖŠŻ†Ņ±äcžóžŹJ§2ķ‹Lšļه¾{÷¢sÕ|ž÷1¬Ō“śóßąµĻ×%\@ZqKzœŌ—i+™üܦ•o’ĘÖ9PҬ3½fĻŻjQ4{ Üł/>/ŚzĒc<ēM¤?€ņņ™ĢžŁŒ¾'ĖĆZNzœkœœŠ]/)„Ķč~lš,Ķ+Ÿn’³(+ĀjµķHžŻč3`{vmFŹŖOxå/ēÕ[Ÿ·¬čKnhG~Aģ½×  @ėŹŃ¼31¶īć]WńöĻŗJ :1šŲæ÷/ hÜėpÕū†gߜAjŲŽ>æŲƒ`Üs|“ ÷:Ķn5ŒśĀŌ—ŽfJIl¤7dc†ŠbͼōѲĒ‘ŲĮ½_›K’$I’$éēĆ 2‰Š³?göü#8pąqō90‹`ć ęO~Š»xmg—&I’$I’$ķP>#R’$I’$IRŅy°$I’$I’¤¤3ˆ”$I’$I’”t‘’$I’$I’’Ī R’$I’$IRŅDJ’$I’$IJ:ƒHI’$I’$IIg)I’$I’$)é "%I’$I’$%A¤$I’$I’¤¤3ˆ”$I’$I’”t‘’$I’$I’’Ī R’$I’$IRŅDJ’$I’$IJ:ƒHI’$I’$IIg)I’$I’$)é "%I’$I’$%A¤$I’$I’¤¤3ˆ”$I’$I’”t‘’$I’$I’’Ī R’$I’$IRŅDJ’$I’$IJ:ƒHI’$I’$IIg)I’$I’$)é "·Ć^׾͸qcūܙ;»I’$I’$éæBŹŽ>a»Į—sįYCŲ½Ur#ėX¹x“¾|“ēG>Ēø9;ŗœķ"‘aŌhæ½¹œ!ł•Df¼ČŸ^žż“śś_˜gI’$I’$żļŲ”AdŪAwp÷ƒi@äRÆÕīģÕjw-zq,Ū‘åģrv;äŽļQAég“~RéŠ{’š4“–eŅ“]·-vd!’ćœgI’$I’$ķjvX™½j/“Ŗ`į›ņŸqó˜:ó3Fæyoµ¶C3šŪĻ‚…OsĒ«­8ś„“M/aś‡÷šĖk_®jm2”³Ļżż÷hEÓŗõČÉ,¦pѾłča.¹ćŻŖvĒ’óc.Ž­‚ņļą¦9ūqöį©—±žł>Ā·<ĖĢͷX·|9¹/][×£Ŭ_µˆ©³ßcä÷ńњhµ:Ņ8␛8üģAtŖUĀ“±÷rźo^­Ö&ÆŻŃ\xŁqōߣ5õƒu,žń9/…»FŸK—АH4vĖtZļ›;ö†X‹bĄ1’LŹ<łēń\3 ‚`ś­ōłå Õ>‹|}#{žõõŻĆswõaĮؑ,ķr ½³f3źīĒXōŻ)…UŸż…#/{%īv‰\7€Į7ęĪż³`ŃćÜxO.Ÿ½/]Śå.ā³?É]k®ē±;ū]Äė'Ć b×2·ä(nųų ś„†,xņ8Ž»oaÜs(I’$I’¤š·Ć^VST÷=Ö-…Dķö=—_ō¬õ£Ēõć‚sŗÓ0 !-—üaæęįcV}Ÿ×īH~q@:6­Kndė‹!·IGśżGžæv·Ŗv‘H”h4Jz’ółĶ©]iœ‘IvFC:ķs%·’¶oU»¦łæć:–žbż­(…œ¦mč¾w?z×)ŪŖ¾HĪįœwż¾tn”I$3Nū\ÉC[ŌWÆÉÜõš•²g+ź§ĒĘŠ¤óPιć~F ŹŚ4Č„±ś6Ē”Ńhģ³ņŠÄ.ŃöĢs<Śļw›„‘^§#‡Žø™÷ØCFZ.†œĘ {T&Ō.ŽėVMżĆ¹ųÖĆéÕ6‡ ø„ōģ¦4©ŸĀŚWĘšł†RĆō8¬]UóģaC镁Œ¹|ōśĢ™I’$I’$mæśÖģ1ĻN¦8 ćŅæ½Č‹ŻĢ„Gīń½ķ˳WńįÅūqč+yfA„€ Śļ׫źū¢„ļššWó«ƒ÷¢ßążŁČPNūūLļs8ưZY…|zÅ! Ł«?7¾³‚€Ś{ŸĢ1ٱˆ­ėO«0€…OŠošž¼ßPśõķĻU÷Œfچ­7VäĮģ'/ęĢīᣱśņ·ØoÆĒÓ%Xūæģ8~yŅŒZ!’ŚŒį—Ÿ @aᣜ7øżśöēÆ_Ęv\–~~=żśö§_ßžģõ‹“>Ļq™ó0§¼‡Ė*Sóź9pż[ˆ–7£I»Ģ„Ś%zŻH«ÅʱwrńCŲkČP†?‚'gSXēeŽW@óĆŖšw=  !D§æĖ=³2·īO’$I’$I;Ō "'={—Žxƒ‰+ʈ™4īøĒ_ż¼óļß±«ņ­‹[ņÆ|ZĢĘōńLšT @ZķĶ;üJ ^ę›āž?āÆ<4ņ ž~śin>2öĊĶh–[żķŠŃ‚7øężBBB>}ą}ęE ’҉^=b;õŹĖb»+÷ē¦cūÓ„N”±?ĪØ„©[Õ®{—'ļż”I³žbōwźĖ-9ŠĮ]ҘžŌķ<3žåž?ĀŚ*[äŠVń[žł6 «}šōyŽGéŹY,\²ˆå%F—3gŚzV/+ »EnBķ½nå|Ä£W=Ƨ«Ź Y3{Æ~“žI/}ĮŚ*Ś äĀ6%dÆBß±sĶūśvĶ”$I’$I’jÖ "CB¾|ķĪ:x/.śŻ“¼õõJ"rZČuæ=fėJ×ńie, ,Š”nźds9ō†—ø÷ŗćŽ»+Ūµ£e«4k ’‚0Üڕջ[9¹*”*4‡łA@PY‹z c»gżēm&—D¢ķŲ÷×wņĄėońĘSįŹ#;l3Ģ Ö-ēĆ0BHČŚŅźõe“hCóōŹč ę|³°*TÜ8q:ó#‘Š4ķlÕgMHxžć”¦–RV±±–”–^™žP»DÆ@Ź¢Y¼P­ŗ[†“ėŽx…VDH)Żƒ[¹×ō­UIyŚT>}„`»Ę+I’$I’¤šµCƒHŲ }śĘ½\węÜüö e÷a³õn½ļŪĶ–›õKŽ9Ø!‘  lśH®9õ`śöéĒ!WŽŻ–¦o㨌ŖUęmł[ĢŹ…÷pł)æē±—?aĘĀ ”E2©Ó¦G^óžvXŽ÷ŽgW”č<ļ(ŪwŻ ¤ō{ēŗØĪŽ· €öĆcĄ=iBŹō±Ü=ó»WY’$I’$I;Ć ";um¼ł— ö3õĆń™éä„o½īū¤7oKĖM÷2O}ń>ĘL^ Ōo[—ŒļÉS[w„w;GzĒ.“Ø #ėXµ¼¼Ŗ¦Õ³Žą¾›.įä£öåČ£~Ć³ÓŹ‰G×Cś'4ÖāłsXX©ØOĖNM«>ĻčŅž•±’K ’bĘ;Ļeű&aŚęšÆAćä«ŪsŻ~T“Ÿž„eTt9œsØ Ąģ×_„äl:•$I’$IR‚vX™¹®‡ŽžĻÜ{ '“'Ūµ§kĻ#8żäŲ›Žƒõ‹˜³|ėĀ|Ÿņ„«XÄR¦F:Béiƒ9āößPƒ#øäź”ōīrgžf0 Cج˜Ī_Ęśi>š®üE’ŖšnÕüŃLZ{ndfX¢µ>ķY>˜;¶ćÉWš«nĶhÓī Īŗ¼µC–Œå¶ŁÕwė­/‰ŻĀœŚŗ+2¶~Nb<™ēĀõėØh9Œ voCß}žČ¾ķ’Dn×u‹ĆśÆ^įćł"• ؟Rž6·ßž_%K’$I’$©ğüżd"dÓbĻù ÷”›^̉Dƒ½ō$ļDāĻE׬½—W§Ē„Sipų}<ß}>µóiž÷żAZtYŽ¼•{‡ ˆkŻ»óļu)@Nƒ=9ź²ć8ōĀrJÖÆ`}j}šŌJ' J˜2öóĆĄ{7<ĆaOH§Śƒ9÷žœ³i¼)‹xū¶G·Ś­7żĖyŠÆ-A½ćøóc(ÆØ xĪ#ģŹĆ œ8žyžņѬ=b µ#{pņßŸąź›i IDATÄh!%k€Ś‰ 5ŪsŻāQœ>ž7ß_Āa'6 „ąuœŸęŽHI’$I’¤]ÄŪ¹±ÖXžqOæ7…łKÖPQ%ZQÄź™xć/—rĢ3īó¹ĖGšļę³Ŗ4›¦M›R1ļnæėÓŲ³·”tś’ń‡»>gnQ9Ał fŒł׎üqUXµxĘk¼öÉ4–‘‘ÓŒ¦yP“lŒ¼ŠS^šp}+ü…Ė.¼‡WĒ/£Ø,…hE‹gæĖæ/:Ÿß½·~«ö3ž¼•?ŽšĀ¢Ār¢))¤§§S+7±¬8‘y^śŽÆ¹óé)¬(‰PV¼”‰ĻŽĘ ß$I$zŻāĄÜ½ĻüMåO{}Œ!¤$I’$IŅ.$hٲežžæūRĻ%+(ūč ]žįĪ.GIŅ Ćžöäpš•Oä”cOåĮ©;»$I’$I’$m²Ćߚ-%Ė~ē ¢E%T|óŗ!¤$I’$IŅ.f>#RŖyY«sŃ»×q`4Ņ22 £+ūųĖ;»,I’$I’$}‡A¤ž«E6d’[+Ģ̐ā s˜ųÄ­\ż^ŁĪ.K’$I’$Ißń³xF¤$I’$I’¤ĖgDJ’$I’$IJ:ƒHI’$I’$IIg)I’$I’$)é "%I’$I’$%A¤$I’$I’¤¤3ˆ”$I’$I’”t‘’$I’$I’’Ī R’$I’$IRŅDJ’$I’$IJ:ƒHI’$I’$IIg)I’$I’$)é "%I’$I’$%A¤$I’$I’¤¤3ˆ”$I’$I’”t‘’$I’$I’’Ī R’$I’$IRŅDJ’$I’$IJ:ƒHI’$I’$IIg)I’$I’$)é "%I’$I’$%A¤$I’$I’¤¤3ˆ”$I’$I’”t‘’$I’$I’’.eg §ąź_¾©0{łgOŁŁåh Ī:ŽĖ…åÉ?qüĪ.G’$I’$i»żW‘• Z0óÄŽŠ­!“̌}ųž+äßøt»ŚżģĮęŸTpšžŠ*„iÓČg}öż³“¤k$I’$I’“£żW‘åMŪĮ”­j¬jČ~Ż”k%LX ‘’$I’$Iāæż‘å%0u><’,žcń¶“$I’$I’”»ÄŽČ‚#ūAę,ņG.Kčø“oʑį¦>ņ{@“š'µKÄź~}Xqjh‘A ¬.„©ÓČæió³ ®9eÓ3æ‚7ėĄIM!­>ž„ügWµ«lŠ‚™§v†nu”Nd”Į’•šŽWä?°`s=ņ+į“`A;8 !¤•Ā'Ÿóœ„ėŪ,YƇQqJsČ-Łf„m:0÷Ā= SķX³ę“óŠĒ4™P@$ow¦ż»w¬q“2öwĻż(µi®ē|Lž9›ĻX}?,Žy‰wž©/Žv:v`įŁ[Ģß²„šļńäæ¾¦śXĪ> Vśxķ#ˆžōµ*I’$I’“+Ųé;" Ž:ĪjDÆ–ļģRāV¶Ū VÜ“;“ß>•F”Q=Ų;Ÿ¢–ŪŽšu³›Äž– { ¦ąŲĢŖÆ‹:īū7‡ĘY±žJhŌŽŻŸ‚ źlī'höčĒ6‚Œ4Č́”Ć(øŗŃö××°æn S7õ7˜‚c2Ŗ¾®lŁ¹÷‚nµ!HK‡Nķ)¼ł fōeŁa¤Ź7ÕW%ˆż@dóNŌ„ėū1qĪK¼óo}ń¶+o·' ’ōłkŽ~}'dUµ+8åH8¶9ŌNƒŒ<8j?˜žų|H’$I’$ķ‚vźŽČŅĪįŒlųėK4ųž[ö~ĶÖ`Ń:œņ%•9µ™1¼9ó"„ßIĘ:¢W½H»Æź2ż’Ž€V©Š»><3€ō…3ᮉtx}IÕ!ÓO?NlCóį¾O«÷—[7āD”C»ĄmK·Æ¾œ<ō Ķ?ÆĻ‚śC½4Ų³ü'VßĢ {@F«ē’uŪĒŌ[Әł×…Vu ĻļŸ|«¦ŠįąŲ.ĄéwŸ{FäoŃįŹłU§łö¬ ׯ™—xē9Žśām7ū¢Ī±ł[8‰&#>#wv%Ó/>«Ē÷§Ž%%lՃ „e³Ø}ē7¬mß Īnŗ}s!I’$I’“‹Łi;"#iĶ™ū»öšŃ»txmćö‡O;C٦æ“`śŃM ‰®„Ć&o{KēŃī³ŹVĀ܊Ųgu7ļˆLŸ= "Ķ™~óL’Ē‘LōŲÓ½zy[÷7ė:¼_AHHŪ'fÅ>KkȌ>•ŪWßź:<¹Œ¬©“” z})ŃŠ%ūģ?Ńü³"2g̤Į#óbŸµhĖŚü-’T æēóDė‹×ĢKÜóo}q“‹fu†®)”ĮŸ;»’NMŽ›ÓŒ‚AP޲ Ōcķž|ŸF–Óįé7ą›ŹķŸI’$I’¤]ČNŪ9ķš!Pś5nYśßBmßü†Y‡ļ é ąÜįœ] ó—Ą3ćɵvė ·ZK+ˆå曧~ś•ĒĆž™@ŪaWM ō!cÉ-[SÕ_śģ•±ĻĀlĀśĮöÕ·ŗps}…Õė+oš»8("µ ˆpSv]gś2–ÓĀ,–5©¤vA4¾ÉŪžśāõ#óļ<Ē[_<ķŠ[ׯTG*\u:Ó½)XŒlśaŌK„tż¦[“ƒõŌ+(«šgfl„.ŁŪ?'’$I’$I»ˆŗ#²ąŅ_PpOG6öżÖŠąžėBH€čģÆiuī›šęXZ ŃthŻ ®8˜¤%ŌWP§+ģŸA_Šō¬ĒÉßēŸ4øq‹[šƒļĢQźęŠ/ĢŲ:K®Éś’!iõżĄ¼$2Ļń֗š8ŹJ”ø<ö³” Kb?’$I’$I?;vGdE]ś°ąĘŒ|–:Ó~Āów¦ŅęĶ'’¶XˆUŽø%³oķ3 ‡5…QsāīŖ“IŽęŻyÆN {V¬’å ræ’  Ų¹7“kø©¦"XU Dk“¾”Ek HϦ¬]:LˆŻ¼¶ż·ēŻ@ƒ%[m/üa5X_5?0/ Ķs¼õÅŃ.}ń¦”A1Ü>’ü1ß³Ž[nˆżę²²] u§oŚ9Ł2sŪķ%I’$I’žĖģŠ‘m_Ÿ aĢż„üĒ rYÖ°!Kz7cyÆ6»©ÆŗõYŅ»‹z7Nø]¼ī͌£ʂ­R–Ģƒ•›‚£ŒÄʔ¶²pó/ėÄīNk‡×łžƒvdƹM)źŠ†y·Š}V¶œüqŃÆÆ¼|:LŁtģ±CXŅ=‹āÖmXzś¦—Ø,˜MŽ“ļ,£ĀņŲߍn³Ļš¬Æš˜—Dę9ŽśāiW¹j"±[°OķĻźĪŁ@i£ŗĢŁkfÜŃ7örńł `e»…ūä~¬Ł­sößŗļōŪK’$I’$Ոŗ#2Z0ŽęgL'sĪŹm<£/qs ‚s7½däŪŪ—»ōeż-eĢøõ1ŚānÆ šĮŁ{0ćŒr(ŚaŌŻōB’±KCøōK˜Ü:Gį C˜Ńq=a½ŗ÷Üʐščį,: ˆlŖ{Ü7UsZ“õ@Ė|ɼ»zBŻV¬æ­%ė bē ÖĮ’}³õµœµśåAÓnĢxµKģ³9ŸŅž‚©5_ߖ~`^™ēxė‹«]-ž6™ł·īĶ:±āīŽ¬,Æ L‰ĘnO™ @‹`Ōj81ubłŻ!(‡Õ%P7}ūēD’$I’$i±c·[9·fBČX›±Č/łö³h@I°]œšMš /%,Ŗ$Ģ®EX?„eKą‘Qä’»8įa“’żhæ JŅ[dĄ¬Æąo ¾’€‚OąŸ `C”ĮcČæaóŪljŗ¾ōi_ŠäŹąėuPŠR˜>“Œk^!lŁVķ;<žŒZ «*ÓSc?i›'¹¦ė«ņ#óļ<Ē[_¼ķ2&Ž„Å%Āg«ac%az*T”ĮܹšŠŌŖvł= Æ,½0زŽś II’$I’ōæ!hٲežž±³kя(ųŪYŠ”>yüß.ŚŁåģ2œI’$I’¤]Ÿ “$I’$I’”t‘’$I’$I’’Ī R’$I’$IRŅłŒHI’$I’$IIēŽHI’$I’$IIg©Ÿ„ĘdņĮō ¼uėō]Š$I’$IŅĻ‚Aä’ ę÷ēńĮō |śętŗ”…;»œ¤Ūr¼=Jāo Ø$$¹øŸ(÷¶ŲŲ>y:}ƒŹ]Ž$I’$IŅvKŁŁü­~‘ĮIǔҽM)uk•-)cį„€’ü1“¦¦īģņvš ²ÕpĢÜį”L†·-&cān6³f;’ ’5Ž]Į·c "°‹g¦’$I’$I?h§‘©Ó8ķō>»m$RÖDųč‰敖P?Ż/ˆrPću›?HŅŗ?\õōjӌǦ'֟~XócS9®ćrR?\ÄķĻvŲŁåü$#÷ŚĄHr€’īqH’$I’$ż·Ųį{ČZÜX‹ē^Ē)ƒWÓ©~1ėWŅ©9gŽWĢĖw/ ]Jü·oX^Źų\v`ƒ:äpĀš2‡T*3pŹés’7I’$I’$I Iéuā\&=Ū1é' ĪĢćžć—Ē–¼#_Īc~YK撁+Ø{P-žŗf.ŽhWo]Ę[dWż>’Ńu¼pr=.mµ„z-·ÆĘŚGdsĶyéÖ¦˜¼°˜¢“+xźŖ\>X Å’åńŌ°dĢ[ÄoGwēų“ÖÓ1usŽ(ęęĖ3¹"vma~:^VÉ ī%4­_B-JŁø,ŸÉ©ĮvÕW×c[>Ķ-Ż—T,嵓špė„,Ö ØÅK,§eUķJž]Ģž×ęW;6Žė‘ČuĖģ•Įł[ĢóŖ•|ńT®’Wvµswūc&W“žik™’tĄ_ĀŚ Ļ™$I’$IŅ®h‡īˆ¼čü äQĀāJ8欺<÷R„13Rhæ[)įņ6’Bķć+9£kȾOgóÖ˳œ•Ų :źÕ/`ÕüÄė+Ł»½}5ƒŪ¬#"–•e’Ł `½VЧnŁVķK[4ćŠS—Ń$¬ ’TZ Ļę·',«ś~qÆLNŚw-źo /,cid6H”ē/Sųėu³ŖŚр(ī׀ŪN\FćĢR2£)“9(“;o*ŲīśŹ5ęā«W²{^!‘h*mŹąŗ-źŪŠ+›ūžŚČŻVѐ2*ƒõ»D9żĮUܼw,h ƒrRˆRż…ŃM?å›?O“¾š¾af@ė»j1¢ūrRYĻG5ä¶ Ł[„ź'²õÜxÆG¼ķ*dsēwę9ÆE Co,å±3VVµKQ›;Ž^EĖĢb2£é䟐 ĆV"I’$I’ōæ`‡‘ÖbHĪ:R×.åĮ;V}žŪ+u ÉlPJ•TFrŅČb~ß})ŁS“ŪĘøĻ9£6‡dÆVņįÓM®±éįŚPJƼE\Ų¹6GvMcp‡\.»·Sж~œfe°‚ĻO«äĄ®éüsu}B"“=pmÕ÷Mflä‰Ó9{P;ÖęąY ~¬!!õŖ`p~§æB¾>ƒ}:dsńŲ¦„„dæČ)ß®śŹÓ‹Yņ`§XŃ…y[Õ7lD„n‘¶vŸ•Īń'ÕåłUuƒųĶ"ŗ!¹¹¢C„Įrł}AlNS>ZÄą¹ īĖ ƒ;295²]õÕōõØn-ī9d™¬aĀՙüęķZ„Äę¹īG…œŃ!…Įr¹qĆmæ„»ń¶:" EóVpĖ‘é īĖło4¦„tŚ_²’#s+8ļ°dQAڲüżģ,.æ·)åµĖ·ŖK’$I’$éæŃŽŪŁ0BcŹIY£‚Ķ·Ō†’Z˹hȒ-Ž›“–QA”UŒ:Æ.ü<{[½m„xhæz5u(ełæaÄē¹ —XVŪŪWŚ2ƒćN*e÷ŗ„„|voČėK¶~ wڲ^—CHȇ“co‘NĖŪü}īg„|Éįˆ{£<0Ŗœ‘ļóŌįkbēŹƒęµ*Ŗõ—YPČļŌ&$dōƒaģy—A{ö*Ś®śR×®įń;ź0ć³Rž™š³U}‡t\ĄĢrxč½4Ž/ćÜæd³†(%­jSLøåŸM›SƒźŸo ś­ļĒ$Ś_i‹Ę\}éźR¼;R¹čł:UµĮwkžq?v=āiWŅ"‹CZÆJ™ü×l^’&‰14*3ź3¬’:–õŹf÷œ" ‚)÷dńÄ»Q>»·ˆ» $ŗžó·##¬bŌu¹ńųĀĒ®™üéÆŲ l³Œ£®ogŌTŻ’’”š5YTR—æ­ąÆńĘ«¹ęųĀmö)†u›Ā²uŪų¾Ķ?rųĒu+8°G!»µŁH‹&e4ƽ9ÄŹ­]ż¶óņ„T§Ī‡…Ģ"H§~ƍŪU_t-¬(  YVRż»¢^¹“¦ŲĄÜ‰¹Uń\żIeĢ!ØGÓüųw£nO}5Ż_e!“J JĆ.k·ėœ[ś±ėO»Uł)“£ H£óķYŒ™^ȻӋ3½žiŌk“‘ŠŚ)Ō”XĆ“I9U×㳉?WS’$I’$iW“ƂČÆ­e|y6eµėsŚ)«Ŗ}²īŻB®’C&͊ņņõøń­Üøƒ¤T2$mĘ.ę² Ūow•õé.9.ƒG^«ĆōŁ”%·mßXʃG'ö¬¾ĀvYœ³×"„D'-gÄŃ ŹĻ”Ń Ķ)ŽcŚ7“Č$ć;ćØÉś‚jO|¬5YßöõWŹÜ³XB „q]÷Ÿ>ØM¶u=āk·yžK ).Vż•¦PXP$~Ūŗ$I’$IŅ›Čē#[ńłČVI?QHȟFÖ¢˜ ņÆÆäŸ7¬¦oJšu‰0äź,|s##ZpŽš,n}+'ī0ńŌ·29ØžJ*&,āš3Ū1©ō'l”}Qʃ—”qF8ō°:<:·>Jē£ ŅÖ·śv'\’JēķÆS €6­2Ųö x‚|č³é»]ÓhK9PĀŹå™5^_Ögė˜K:E‹.›»SiM9°’Å™ õY“õmOiĖVš§Ó£Ü;©>P—į7-¢S4±—U;ż]8ŚåĪ/g Q ”É—”2¼KĘw~²9ķцd,Æ`5Q –ķ‹«śŽ³łö×/I’$I’“+Ł”oĶ.¼i iD Łt8!;’]ĢČē ¹łŒ“ŹØÓ¶ŽŃŠøCČ£ŸĶęŒV+H[»œē’Ӗģ}¢ Ų?`Ą~CŗUüxߑvX&WŸZBėZ•ĄĘ)%Œ[ž@f‚łfĘņŹMĮR”FKŖ6Ę]xč÷ßī\ŚØ>—XC·~©üłŖbSN„5|>!«Ęė#€W b·¾·?§ˆS{—ѤG½dyT¶,äµ¹éÕY]ŗéŠ|”¹õüÖh}ŪŃ_¤Ā¢J^ųsĄ4Ņ:6ąŖ#W'~āM~ģzÄÓ.gzcęÕŅčvY1Gõ-ƒjµŠķŲ4®zŖ”#ÓŹ©óu!ßeQz^±š£÷¬¤ķ¹\²ūņķ®_’$I’$iW²cļ `ņŁė9ķčzœuj)];m 2ÖĢŖ`ĀćiÜüDūŖvńč³ū:BŹj7ą˜[J9ŠŅŖļ²¾ZĄžĒvHؼ -38ųā žmÖĀźŒTš¦/*˜2¦vB}ՙXČČyyÜŌr)9'Öć?ƒ7R”—A›Üe|_ž›ŗ&Z'Fø÷ĵ›ĀŲ ÆG¹6‚š­ą*™ųD»ē5ęŒ‘ÅœFB`£߄/*Ŗ×łłŲŹ5ć–Æ6RD&/ā‚ƒŪ3%-Øńś¶·æ:¬ē¶±õyh@)Æ\āoÕęõµ)÷fg·Šķ¤L bĻźĢ8²oQT²ž™åy]ŪŖ~~ģzÄŪī©[£ņ·L:“øģ‰bĪKI b7ĒGXĄża[ą’^Ķäć"sé“ÅTRĚuP+į©“$I’$IŚåģŠ‘0’¹üī° ŽźĪ°95¼67?™ wŲ9l±s2 ’č–?Ū1²š«¼śis×§“V;•éŔ®(ćӇąÄ4Jøæ1ūńųųŗ,#ƒ¦-ŠÉ˜¹Š?ÜŪäūŸ9a!W’µ³Ė҉²ł£6põõm«ę¤¦ėĖü“ˆsĻŹäÅÆė°žtRĆrVĪ*įł³źrĶ6^¹y æ]Ÿ„éD€Œ ¤VnH$Iõż”ž>łsÉ¢¼NS.ŗt!)é2‚Œ ÜøĘū, VjyõN~äzÄŪ.ķķõœłĖ,ž=¾6‹J3ÉBŅ(aõ¬R޽»9Ÿ{!Mńuk¹śÅ,(M§Œbę>SĘ £ė'y#Üį,«żČ!F~µ²­žz®`ūč…Å* (@“źõ²³źĶSź½ķėĻaū‹ŽŠ,b’õĄK\ŗ}…B!„B±ę­ź…Čś‡v ·²xH‹†‘AøŁÄŗļ^"¹n*Üį…¢Ģż3ż;=w%Žŗ…ķõ” ÷§¤mbäse=ż‚Ó .ķ¦(ąšōü·u:-¶0BˆrR!„B!„«ĒŖ^ˆTŻåó®’aG5Żé˜?ūsā›eŃĆPo…ĶSp© Xˆ-Ķń,Bšī’ōŁ’"ėźŅ‹¶§’ÉēėÖŚ·°½ßóßöļ|³P*„B!„B!"Īźž²šNųŃėä>ż=l}‡øæhņ¼ŸOūń“°†&üI“Īüײ‹B!„B!„bõ[ÕwDŚžąg^?ē½ö:öcæī¹K2;č×½Ļž=»čż•(Heś‡įę-l:÷¬EūiśŒµšJ*|,¬šĪ9lŅ8»ŻTf·eT§AjÄø ³Ž®ÅömsūūŸOƒm Ī„¶Rxß:°:įÜ9l_kŅߜhŽaņ—ņ!qĀēžœ%å4’vlLö쯔•„ļ¾CĪ„ L)[øõŸ;<GM/n{ūĖÓw6½ƒķSśžCi’ŻĀ£Ń`š^Ÿ*ĄžņÆzž»ē¶Ÿ÷¼¼~/·’捎o~ć9l_ļÓu¼łF7”sē“óŹŪŻ’yŪK^Ūé«g’쟒ExŠ Mļ‘ōļ1 žb1ĘĀČ|ū9l/95Å7“’a:æ’ÆÕBåfˆé’s Ż ¼wŪWšfėÆ}gėX$ś‹?¢š’§MfŪ]‡Æ’ŪėśźĻžÉ÷yņ8z^< Qr§ŖB!„BqÆ Ė‘öOÅžPœįūRr }zĮ£L÷ū]čżÓ-P6½ų䌂¬t8lc¤ŠĒBJ^%|2ĒóßÖx8tūGbg=²”ɇģ8Ļž&ČŹ†<‚ż·Rēöc2A” ŖöĄG² Ę ± šĄģ_Č <¾u™ü\ ¬³Lļļ ö'cf=UXMó?€źdĻG¤­Ń°±ŒįƽŸśŻž5jÕ4 īéųf)žŸ£L` ąćļQSž÷ĪėĢž¢&g_ņ:v0Ē›ę.ŻÉæYPŽüBųÜū±’Ā\>ź®g-LÓuV°™Į’Q %ń09IɐfŅŸ2½ŸGīƒ| däĀ<Ūā 6öļ¤e›gĮXKūZša L)LģL˜ ·{[¾ē8Sݳ‹ZėĻžKOĄGņ!Ł 1)pģaŲB!„B!īmį¹#Ņ’_| {ŹKŲž4l·moó,Dšī’ų|7z×Y›ŽxĒŚ/QžK5L%$S4„* ”b‰śüQZ›FŻ’ž0Y`GüØ€č;·įW(©sö-uæö|4°Į’:ļ½æÄ ųćŸP~b’ŗÆ~ÄĄ•šõ®ĄāK0ĮwŸ'’Ż Śžx/¤[ag&<ć‰ļöoß1 ō7÷õwHȦõK¢` Lß ™ž²x ×(?1‰ŠŹśkš¼f]Gż®©Ąāė·SžļŻÄݼvļųĢQåP9]GϜ&’ā±õ·ÉüēĻkėqŲTę’o–ŗÄėͼOQæ6Įc”ØøM°Łģɏ;Gbć**æ{ݳABöÓė­g=F(’²}¶\Ń­mä]šŌ@÷ QƒC0¬#d_qAūųō¶V]ķ›yzz”|}f×:Ų2żüĪ‹mŗźĻ]˜Ŗg»?AÖ„Źųsø&ĻB!„B!īuį¹#r¬‹ņĻżĄs×ß±‡±_ž¶S,ĪČ® åĻ6A pī-Ź’žn@ Fė_¹FƇvBt&|ś(öO:”µ~tŪĖŽÅo›;ŽsĻŗī\•ÖżžSšH, x 3ćŁÓ9ļ„īŁżE7N?QGĶP‹Æx.¾aļųܹ)ž×*#Xģ#ØÓkŅ©uŻōPjŻ9S$ۃh—2^<żåEŖ>’kŌ}nzalę9•j ¤[—žzÖc Ńb«®ųę=S™`Ń~H0éjßōwšč™(€ų,Z¶MQt'rT0õsj P4Ēēšłłév×ģq©ƒŹų *N!„B!„«]XīˆTQ«ÜÕ10ŚMnka$b’ś>HTą½7°}©%ą»Ö¢/SōéWą•6črBT4Įļ}€ś÷Yżļ`%u3<ēy¦Ÿż=rż{Ųü™2ļ#ĶŹ‚8-s‹~jĢā5b#ć»§¹œ0īöüuĀš„ēß“Öó„3čųŒ6鬃ś)Pć™Ų‘@Gu¾ē]wHhōńLĪŽO!„B!„kCXīˆģß?½_*ƒA;9_>MBs`ĻŻS¢sØūŪƐ\;ķ÷}ÜyØg‡`miÅöuĻb”;»Ę?>e±ØGrįå&Ķ»rę¤ĢÅņĀ%ā<ūļÉL\śMėϱGK×MĒ4w§€(Cć3·Ą«4.y>ģ(›9ī(™ĮTfL(€ ‹ £;¦ļdTĘį/æķĶ%ņMĮŠzÖJk|CµķOWū*ĄÉAØLŠ†²¦ļ~|w®?i®æĀQĻ’«‰ō•šI«›¾s²0Ö÷öB!„B!„øg„åŽČŽ]ŠqŪ/œ ”a2°EHSuń ›”枢sg;<’īVÄųßÉwī žųŗŁÅ(sg ōM/¤Äč‹ŃŚ7<÷ƆTϧ³­š”Ō„ß“nõŸĪe¤¼„–Ļy^sõ`;ex|nwܘ~ļGī§skćÅ%tżŚō—Š“5’rkAz {¾T…¬uŗŽµbīNĒW^ŗčWSwƀ]ń|„ųWöŅæ)pf„Ńt؊śæŚ=»čfd=k„'>-ō¶oŁłé;u7lJ«güō]ŻńEµ¶AŸāł÷Ē÷0P‘DÓ#ĀÖ° 5B!„B!„ˆ a¹#Ņ|į4%ßh źīESl6lŽöüZ_(dhžWß&ķæßÖµĻѬ<ųdõO»ad Ō8H›ž‚Ž3]ŗö„vÕĄõ­°) Ž’Aź7 ”¦§AŹ2 Y}*źń£“LӕsöŚl= ~»†–olƒ“"†¾^ČŠēøŹ üļk‹Ū§aö¤@n5õ/Tz^k:OŁoŻŌwlź’ī”l:ėt½yœśnP&įå(ū»ŁķSŽģd`O.¤ŚØ”ÄóāŲmŹž< |ė:­QyéżęśÜ“Øę(ĻĒēĶsßmh=k„#>­ūÓÓ¾ęŪ-ŠQ9ɞ/›éĀvV|““ĆĖżšŃČŚHĻ77€ā†ž H‹¾ž„B!„B±j…å6„’SMĮ}„z†2ļ?L>žé”wõ&¼Ó…:2…Ÿ„š”Bw'üóĖŲžs\÷žŹžč øp&¬Ø1ŠP ßj[ś ösš6ēœ|ŪĻ}›ŃńEßzœß? —Įm‚I'ŌŻ&ę‹Ļc;ćZ“}ł÷ކ—»ąī$j“ÅóĻŗR¢g3Ū֊2}ÜŌ8ļuōĢ×_‚·Ćš¼ų,³æ¹r†‚ß9ūalʳßI47ĆwēS®g­“ʧ•žö“tBĶŲÜ ·/ākĻöŻĆó]ž/Hš†WŽ„Sņ I!„B!„ā^§ŖŃŃś?Ź,Œa’ÖÆCłœ{ Ū—ŪĆŽB!„B!„†“· !„B!„B!BN"…B!„B!„!' ‘B!„B!„Bˆ“gD !„B!„B!BNīˆB!„B!„B„œ,D !„B!„B!BN"Wkń!žųČaŹcÕp‡²bö?ĪƒÕ įĆÆuņćwÆŲńōÖĖJĒ.«%_±ė±ć¼æ*-Üaˆ{ĄZīGBå^9ÆĪø×Ź»’ā‹ńįŠ5æ·ømģņ8Ū¢C™ˆD÷āõ ‘¤æ d!2‚m{ßqߑ5ļeśßź²øŚ)«¤ČŖ¢ (ĖjMµ±±²Čćé­-ńjaūV=αcĒ8vģĒū GÜCUqRH޽žz1²=‚”µ(ŠŹŹO:ƒéæbuZ-ćn(IއWøĘg=Ē åy5­ō<Ē—Hļ—Ę§Ø¬Š\ŗ×Ū7r„ę$)ób­¤æÉqÅź¶ź"sņ“ĆBHÄä"7¹Ÿ–ŗŽp‡”µRz®æČ³?ygŁm,ÅTT®PDŽ“Ä_ķ«( ŹD; 57øŽŌC]‡mūĆ<ŗ3²&—įl‘Ž?"=>!BAņ>üĀ5>ė9nØĪ«‘*ÜóœHļ—+ŸĖlēō3?ęUūĊīķö½D¼X+éor\±ś­ź…ČųāCģŁµ>Üa„DńĘLLŻMŌ ®ź&Z3å¾-پ®njnp揻œ}ć9Ī6Œ[¼•¹Qį 4BEz’ˆōų„É{!"O¤÷ĖH/ŅIż =$_„XżĢį aų”āŠ0'ģ (c‚ĪÓ;7ˆÉecÕ&lÉLE p÷śyĪÖ{me-¢bwIÉÄ*NĘŪ©Æ¹Œ}Šķ9†ŁĘm@mlĒ”—GÜd7×Ī6“pßNŠć ֟ēč~Æ}–n»[n±Vw8hæq‘Śö±€Ź\Į}Ū‹HO1crōŅpµų];Čh~“/÷ūŲŪ\¹vŁNĘäU^xµnöõ²Ķ{É.H&Õ‡%Čņ&<đŻć“œ$zcé©*ŹøƒÖšÅåŻvōEÓĻQ§Śłé[óÖĒŽQbVQL&”É$žxā‰Łß Žz‘×ÆĻżuĶ_9ō֋–ųfŽ??_œ“£ōōŁ9w¾uÉcśĶÓyŚß{‰ž‚'É-Ʌvļ}jÉ«@ā›yßĀzŃÓžśčĖ­õē«,Å_żķzģ˜Ļ~•XšīžĀžĀė\[ü#śÆ–śÓCKDāѝQŲOÖc²m¢89nÉqRKÓÓ¾ęéBŪŽ#Æļ ~vaĄēļćĖāįŹ”Łž¬§ž÷?F܍WøčŲĄę-¹$Ē©(c£tվͅöIŸĒ[j|ŃR^=õ§gü³¦–±es™É±˜IG/M·k¹±`Ÿį8oIŽ–÷ o|ćĪzޫ弪§=-EO¾\{įuģóĘ’mG‘;t’ēO÷č*o(Śm¾åś„žüÓ:ži_‰o¹ńĄ’T̶½äʙaŹAĖõ‹\nņ>fюćl+˜šżyčŹK¼V?ī3.­õ쯼ĮĢ›ŒžOkM§\BčŪ×hŅßBÓ«įü«‡–qÜČė¼]#«ó?»ŠķGlś>ŽJ£ūēøŠ®oį<ŅĒ«`­ź…ČpKßō{7$bQ\8ŚĪóĘłn’oŅ ē¾\āĒxŁĒE”‚…ŅŻUŒō5sóF/ÅEälŻGu×ĖŌĶM&wŁInL­·.Ó3OĮ†B¶‰‡—O`SPE‰&)n^ķ K1Ū¦ŅŪt#åToŲ 7NĪīoó‘Ēؐ8BKŻMzF 9ĒFĮžCğz•3¾/^—*‡Łlc×”JR&Ūh¹ÜĮHLå÷UeU˜ōóȎ‡w5YĒĻ^µĻķ/a„eqŒ·µS?ą`l*…œā"Ŗ§Ą›ÆcŌY^EĮ¬f±~—•śš3œPH.ŻJÕžCšĘ‹ŌöĶ "×/¤U…”õ;Ų’ė;ų[M…ų‚l+› ńL-m.϶½½sƒ«–rč©­ńl=²“bSõ·ŽÅį“—“Žu%™ĄŅ'²åņŌ—ž!ČLÉšŚ§Ö¼ $>š]/ZŪü÷#@W¾h©æØ8ūV’4ŽFӕNF¢s)ŪU…Å¢²°¦µŌ_Ē€›üĀī?œCĢĄšn4ÓÆ@JF1Å%Üh×?¾Ģ'y¾¼}㳑ē=ĒÕr^ÕÜȦ–¢5_eńSęL˜<;ĶČyŪ/µ–w¶|Ę?­ć‹Öų“ŽŹT „»ćč“s«ŁJR‘²‰| wēöŪrńYF›3ˆRŠØøæóweh­g-åÕÓ¾+1Ÿ ĘZOŗ„Š·ÆŃ¤æĶ¹×ĪæZiĒ¼Ī»Óź&æd=,XˆL)Ė z¢U÷"$Džx,Yˆ PTR;6%­D“Rø‹½­/r¶cŹß[—eŽŁDi¶J_Żēļ•©œÆp²Ę³ĀŻŲ:ŹĮU‘Y×=Æ„”=Hnā o ¶Ē“,m­£Ü’h5eUiŲĻĶ-H ÖÕbožb"3—ķėŚ8q¹Sl4łE…TeLq¹×DJكlHįęŪÆs}`:łZn3l}Œ­›Ė ć–®r$W•‘iźāźkļ`U€ŗāŠNė²u³õč§/†Æ¢2w"˜~—W~:ĖŚZGŁūh5¶õ‰Ųkęž „¼3õgøÜģ/½NRÖqÖWRūfŪģ6ݽLd.}Å0ŽÓĒ80–źÅEmĒ]ŸŪé)‡–zŃŸ%ŗŠŒÄ śĪŸįróLžŽęĘÕ%ßā7O}u‚ŗn®µęU ńĮŅõ¢µ=ōō#Š–/3–«æģņ RĶ]\=3Ó?š§ū‡÷7õi­æžśAœņ©N¼2;95GŁČŹQq\æć³ģFō_½õēŽص4žy=Źö5šō·9÷āłW+­óD#Æóŗnw0bĖfoŽÉk=Ø0ĖŹXW“ī2Dśxey°B€,ńi$Ģ[óQŌhāÓ,Aļ7yS©“mœ°;}ž^5 Ó׹@žß¤«žĮa…ųųøŁm2²Raģµ=&ÆķZś 63on_Š“··” **¦!.ÕB¢ÕSĄģ‚TG×ęö§¢ŅŻ<†æNw9r²ā1 ÜĮ>ŖĢīk¤½‰į%渊%‹ŹĆ¤4Ŗ×ēb›źć“®zś]`N˜«­å©ēŽÖQÆ}öw81§äū<örn3Ÿ”Ck½h—§j%.+cŃń—ā/O}19P­”+:óJg|ZóeįĻ ·ÓŚ@{¾h©æŌ«¦ž”µž&:ŽĘ1‘@Fiāģk¦‚ß3#\ē-ÉūĄó~~|¾āĻčó‡Ö朌vɲhh½ó!æ n0vžctæ ¤¼ĖZĒ­ńéGōŻīń:^g§sźāzń×ZėYky5÷šOl §†^†°}&żmž{šü«…Öz6ś:orč"½CŃd–f̾“¾—Ģča:n÷é/H¤W;"äčę®’NŚt.ئa;Gą¾ŒcS~< ö%;ŸŠ‹Q—2ļēéēöĢ»Y*& LĆ£‹:õˆŪ –Æż)ӏ¹˜@Ååó˜±I š·ņÄ[½”(ØQ“”ĒØŌ{Ϛ—+G¼\½Ć^æ›r×3ź¬&ÖĒń“ «IFwU‰S^9˜‘Q²å¹¤Ä[™YOTL*ŠĆū.2-åP•QÆzqŗQ-Ź•:5Š«„åi-h«-ÜS7øu»ˆķė÷s,k˜Ž.]nYņ=žņŌ—)« Š»jTĶy„7>£źEO?Ņ›/Fō­õ§¢ŅŌļdWfął+eVAŃż·øīˆ1ŸžśÓBo,lš'A{ÓҾō£„L]dlā8©éQ04Eś:'N%ņ•śuɤ)#4ö»S@õģ^°½/žś‘žņjķZŚĆœH÷kx’ÕĄ2YĪī§ŖČV={›R†øżsļēéĪ?tõ7 ķ«'>åUQé‚’¬d¢śÖ“6ÖDĆh ŁQܱ¤bq÷rmĄšiĀ©åĘ?żHW{hØæ`Ēq_ĀzŽŅ@ņ~qyõÉł#,•WįȗP0²_R^MćŸF+2oW\ķZO=e„ę“+Ÿ?įOĆ!ÜńIóĪöX«ēß%i¼ĪėoøĆHe!ūrMœiŸ¢0ĖŹxKS@óÅHƌ°ź"•©¶¬/ĄįR0)£twęQ±’~8āÅHś[/ņz«÷kĮX_šŹäSŌ7`ĀTźāņʛ­ąņżœA/ Ž=6«JOw¦ķż•cÄ–ųfžZe.#Īź;«ī\ćöŠ1ļ5‘q ‡«›x³v®……iDužēµóó’BŠCnōöe ¹tüž—,¤ÄMĮŲÜ®±‰f—‹[“”9”č-‡æzŃE§ćuŽ;Ō±ėösčP>‡ /šv‹÷sOĶÓĢD˜ģėżYW^éˆĻØzŃӏōä‹QżCOżM¶ŽĮ1^M^A,·‡·‘s—¶†A|ż„ŠØųōԟĖTĒĶ3Ć4N’¬*.Ś}_Gų£§æin_ńi)ooæ‹ŹĢud¦āź~‡–ž4г2É –ÉžžŁmƒļ— ©i,ƖśÓŚīįFH$%Į ½K·yøĪ[’÷Įå½V”:¬4=ó!#óÅ4żü’…‡1öhJ͌ī—3ļ1Ŗ}µŽ/Zć v‡āÖ=ßŅZĻzĖ«É Ģ'W*>Ā9žj’łAĒSéo¾÷½Ņķéē_­õŠė<÷X-½6JJņHPóÉ\ęzK“HƂ“ŖŸéTÜS)WVS½µŠ-Õ±­‹C±¤Q\’į$&ū ¹IĆ4׵ݸ½Ż›GUĘÜBl”¹ŒĀtėńżåĖiļ`2³˜ŖŒé;>ž›GK9:ŗF˜JÉĆ3ׁbóņ¼ž¹éMĘ;/r¹ÕMZŃNŖ“ęŽ;3·ĶĢæčܽä›_®W¦ČXļC…YVÜm·ĖåÕāUīłō—cłzŃmŽq]Cƞ‡ÓĒxohžęl}™Ö zŪg_ӓWZćóŠV/žŚCO?Қ/öøÜ’EżCOż¹¢ģ“÷;I*.c}A&ÖŃnj‹‡#ūÆ®qHŽ;ķܙž×ŽÖ³t~i΃åééoŗĘ-ńi(ļp—g|ė³Ģ µ:ėīa,³œ¢4+#żsߦmōx?Gćų¢”¼ZźOk{ø/Š;MRYŲчė¼%y\ŽĻš7>‡źüįļøFÓ520_œ“ĆøÕx’ÓęīGˆIßĖ:Kp·9Łnŗęõ­ć‹Öų‚Gdg[q÷wėžwj­g=åż#„óIC¬…ńT½ó#Ū×hŅߖap{¬öóÆÖzÅu ““ •YĢ–õ™D÷ßńy½5£hļvķŪ°|"}¼ ĀŖ¾#ŅŁųĻ5zæV“ēQŖcyīB׏ŸŲ‚P“1 SĻ{Ōńq»ö×č(}’²‡°Žģ ›X 7“bźāŚ•»č=Ąąµ×øõ8eū"¶ÉN_æĀTj™‰d˜ėyįÄ\#h)ĒĄ•ŪōäWQ±/ęŗvFcņY_ŖįiH t¼óķGŸ¤t×j§æ «³k„ %ÕŲn”„Ē5”ˆ²ĀxÜNXüdEmTÅITī.łn4„äEŃv¦Ćk»ŒŒt’UP-lJK£Ū¤ *£ōõŒym;ÕŁĻ˜ZFéī &n9p)ŠŃŃ9ūū€Ė±D½h/*qGö&3ŌŃNĻĄ.sŁ IuwSsg’ł ©9O͹lŁ`K,Éé9¬Ke“ė§Ūē{­y„'>­õ¢„=ōō#­ł¢·Ä6µ3“KŁśÅg=żŗŚ8·—`‹±0ŽÖģóųFö_£Ē”€ó` zś›–ö5:>w?Ćę22“ļšl_pž©')O˜ «ŪĮĢ󈍮ēE–čGzŹ«„ž4·‡7.v’łĄ>p8žÖ–œV’³ É1Ūyå¬ēŪüĀuŽ’¼.¾žĘēP?üō÷ż x>“­åź¾H·óIņŖ÷²=©ØL ’q;aį²ŒœēŻ/ Ļ?ć‹ÖųōĪ#Ģ9»8°ŻNK·Bzé ‡čøØ’yjšėYGyĮū†d>i {m<Õ{>2²}&żmåŚ#’Ļæšh¬ēP\ēŒÜn§æŗ‚ÜlpÜlZ¶²3óɍ±`3ŻÄ>å½a¤WFXÕ ‘ ė?*¶‚‹ēŽŖ_U‹Q‰Ū)ʜ ļL£aqŸć]*÷”R\YM‘ādl°ƒkg/sk4°\~ć9Fw¢(w¹å09ģft°‡ŗ†.Żå˜tÕqᤅūī+„tW&¦ž^ß¹Éčż;H_ę dfŌ^ģ$ł ®¾Ć›µō^z‰ ¦CTęleg®“įńv/Ü$¦ź¶€J Ŗi€¦w:‰ß\NU‘eĢAĆŁ‹ŌōĪ-¤eV<ĀĮŠ™‡Ł*(JČd#0„6šÓŸŌxķÓ=^Ė„wŲ²Ńʎ}žĻ? Üx7nx>#l9֋Öų&‡.Ń6tˆÜ\6+Ń.'cƒķ\;{™†‘¹†Ō•§±YŲ*×”øœŒŽõŅšŽ j‹6ӒWZćÓZ/3üµhļGZņEo’Ųŗ­€Ā­™(Ž^/\!~×ŅØæcmÜŻžAVĢ0­ ŽŠžkä8l,¤§æii_£ćs×2಑88wq‡µ|€ŽNļ¶3z¼÷ea?ŅS^-õ§§=F{Oqźr**K)®ČĒ¢8ė„õ¦÷·†ė¼%yx|3“ŒĻ”8ų;®Žó¾?Į·ķOGykĪ×c½Æ€ĀŹ2Ūi>u™ųū˜’]”FĻsĄą~‚üÓ2¾č™é™G4žėÄŗ¹œŖb3ʘƒĘ³©éuT­ó­ć)ųoߐĢ' t/ާzĪGFµÆŃ¤æ­l{DźłW­õläuŽ ×ŌuZīn"%ż.] chł²Æ-"}¼2‚RXXØDG‡ųžj±¤ü±+„‰gŸæīP‚l9ö?yœŲĻóŚ5żŽ‘ ę”=ć\ę$·Āūų؈²VņŌhZó%Ņėo­ō_£ÉxœHÆ?É{ß"½ō˜Ż IDATŻÖŗµšWZ­õó„¢Ģ6ö|ø sķó¼m_]y õ·’d< N$äĖjīo"8ōß²żOR’?~åvhƒ •ÆV÷‘kDTo+Wėn†;Œ SŽ˜“=¤*#twŽ2ó‘ĆšSYU·Ö†ŲZÉÓŠY>_"½žÖ^’5šŒĮ‰Ģś“¼÷'2Ūm-»7ņjykł|i%6…ÅÉšČźĖ“HØæšń4‘/«¹æ £hėæQ±äēØō^ę9ķ‘ ōć•,DF€ę›ś>Ź©ō”£śČ~¢zŚ=ĻŠÉ¤`K¦žĪ÷Éą©ÖJž†K¤×Ÿō_q/’¼į&yµŲZ:_ɒCNšT3eŁÄ»{x¹}õåI¤·Æˆ,ŅßÄj`IČ!7%‰t[i“ķ<[7ī"ž,Dаčw˜±åV’k³‚{„Į®Zž;·śn_ā^$żW܋$ļE(H^ ­¬Ł•ģØJĄ5ŽCƅ‹aŽ(t6ī;L¶e ÷äŅĻ$›2õs×½NÓvgN\[ņ÷Ā8k©ŻÖBÓŚŅ?‚—½‰ķUÉøĘ{h“µ±S^q“Ż\;ŪLĀ};)Žw2Xž7ś½öYŗķ~l¹IÄZaÜį żĘEjŪĒ*Gtr÷m/"=ŌÉŃKĆÕvāwķ £łM^¼ÜļcosåŚqd;“WyįÕŗŁ×Ė6ļ%» ™TS– Ė›XšGvÓrzčE¤§Ŗ(ćZk—wŪŃcM?£Ejē§’unQĢ[;F‰YE1™P&“xā‰'f7xėE^æ>÷×låŠ[/Zā›y’ü|qNŽŅÓgēÜłÖ%é7Oēiļ%z ž$·$ڽ÷©%Ɖoę} ėEO{ųėG /_“ÖŸÆž±õ·ė±c>ūUbĮC<ø{ ū ÆsmlńĒMŒčæZźO-y]tˆGwFa?YÉ¶‰āäø%ĒI-żMOūš§ m;zŒ¼¾7ųŁ…Ÿæ/{ˆ‡+‡fū³žzŽwüq7^į¢c›·ä’§¢ŒŅUū6Ś'}o©ńEKyõԟžńϚZʖĶEd&Ēb&½4Ż®åʂ}†ć¼%yXރ¾ńŒ;č9®–óŖžöt>“=łrķ…ױϒ·=FīŠIž?Ż£«¼”h·ł–ė—zņOėų§u| $¾åĘKR1ŪöVg†)-×/r¹Éū˜E;޳­`jöē”+/ńZżøĻø“Ö³æņ3o2z>¬55žp=”o_£5,|_ ×Q^,e5œµŠōž&ĀkU/D†[ś¦‡Ų»!‹āĀŃvž7Īw²ßœūr‰kąe… JwW1Ņ×Ģͽd‘³uÕ]/S;47™Üyd'¹1]“ŽŗLĻD< Łr$^>}LA%š¤tøyµƒü-Ål?˜JoÓunŒ”S½a3Ü89»æĶGcCā-u7éäūźUĪtų¾x]Ŗf³]‡*I™l£år#1y”ßWM”UaŅĻć7vŽAÖd?{Õ>·æ„”–Å1ŽÖNż€ƒ±©rŠ‹Ø:œo¾Ž}Pgy³šÅś]VźkĪpj@!¹t+UūĮ/RŪ7÷×·ė—NŅŖBŹślÉõü­wNЦB|ĮN¶•MŠx¦–6—gŪŽŽ¹A]K9ōŌ‹Öų¶ŁI±©‹ś[ļāpZˆKZĒŗ’L`é÷ryźKĻd¦dxķSk^ų®­ķžū +_“Ō_Tœ}+Io£éJ'#ѹ”ķŖĀbQYXÓZźÆcĄM~a,ø Ķ*L!ŹqÅē"ärńé鿚źO-y * &ā)Ż^Dóµ œ¼¬^±“źūö³µė%j†<픹æéhß@ót”¾»nJR²ß ‘iqD5Ģž¬§žEĮš½›=%NŚė.Š0¢`M(!#Į‹2Ģc©ńESy5֟žń/6}÷Ī!fąM7šéW %£˜ā’ n“ė_擼_ރ¾ńŁČó‡žćj9Æjn ęCKњ/вųi &L žƒfä<Œķ—ZĖ;[> ćŸÖńEk|ZĒe*…ŅŻń tڹÕl%©ČFŁŽƒD¾Ī…»sūm¹ų,£ĶD)ETÜ_„y‰»2“Ö³–ņźiߕ˜Oc­Œ§F]BčŪ×hFóuąuĄRVĆyP«Hļo"¼d!2@QIUģŲ”L“ MJį.ö¶¾ČŁŽ)o]–9f„Ł*}5vŸæW¦pv¾ĀÉĻ_[G9ų”*2 įŗēµ”²ÉM¢į­Ōöx:[[ė(÷?ZMYUöss ƒuµŲ›§˜ČĢeūŗ6N\nĄM~Q!US\ī5‘Rö Gøłöė\˜ī¼-·¶>ĘÖĶeŠqKW9’«ŹČ4uqõµw°*@ ݃qh§uŁŗŁzōƒÓ'‰«ØĢM¼&‡ß啟Īß²…¶ÖQö>Zm}"ö𹿦h)ļ Gż.7{ŽĮKƓ”uœõU…Ō¾Ł6»ĶDw/™K_1Œ÷ō1Œ„ŗAqQŪq×ēvzŹ”„^“Ęg‰®"#q‚¾ógøÜ<“æ·¹quÉ·ųĶS_F ®›kc­yH|°t½hm=ż“åˌåź/»¼‚TsWĻĢōęéžįżĶ˜Z믧~ē|ŖÆĢNNĶQ6²rT×ļų,»żWożł£7ö ³ķį8÷sr³Ž“Q4;NźķožŚ7Š<õ„· 5/uÉß'¦[½ė©æ@ź9&ÉÉÕWNN·@'K݇²T?ŅŻ~źOO{TģČ%Įq“g_»6ūZkżm®Ģ{wøĪ[’÷ĖĒēžńŁČó‡Öć‚¶óź ķč|h)F·;Ļ1ŗ_R^柖ńEk|zĘEµāź<ĶÉóӯݾ G“WUoĶżįIE„·§³;'¾/äōŌ³–ņjmߕšOj-§F^†²}fōxĮ_GĶŠs°œÕpŌ"Ņū›?Y%>„yc•¢FŸf zæÉ›ŠHl愯éó÷Ŗi˜¾źō’&]õ +ÄĒĒĶn“‘• cwØķ1ym×ұ™ysūRœŒøØØø LøPQ1 ¹p©­žf¤¢8šø>0·?•īę1ŌųuŗĖ‘“iąöQev_#ķM /õ‡&K•‡?HiTÆĻ“„źć“®zś]`N˜«­å©ēŽÖQÆ}öw81§äū<örn3Ÿ”Ck½h—§j%.+cŃń—ā/O}19P­”+:óJg|ZóeįĻ ·ÓŚ@{¾h©æŌ«¦ž”µž&:ŽĘ1‘@Fiāģk¦‚ß3#\ē-ÉūĄó~~|¾āĻčó‡Ö朌vɲhh½ó!æ n0vžctæ ¤¼ĖZĒ­ńéGōŻīń:^g§sźāzń×ZėYky5÷šOl §†^†°}fäx`ŲuśÆ–Ż—ĘöēyP“Hļo"ģäŽČ¹ŗ¹«¤“6Ż—TÓ0ƒ£@p_ʱ)?ž‰ū’^ÅÅØK™÷óōs+ęŻ,“¦įŃEƒÉˆŪ –Æż)ӏ•™@Ååó˜±I š·ņÄ[½”(ØQ“”ĒØŌ{Ϛ—+G¼\½Ć^æ›r×3ź¬&ÖĒń“ «IFwU‰S^9˜‘Q²å¹¤Ä[™YOTL*ŠĆū.2-åP•QÆzqŗQ-Ź•:5Š«„åi-h«-ÜS7øu»ˆķė÷s,k˜Ž.]nYņ=žņŌ—)« Š»jTĶy„7>£źEO?Ņ›/Fō­õ§¢ŅŌļdWfąłėhVAŃż·øīˆ1ŸžśÓBo,lš'A{ÓҾō£„L]dlā8©éQ04Eś:'N%ņ•śuɤ)#4ö»S@õģ^°½/žś‘žņjķZŚĆœH=õē2ÕqóĢ0Ó?«Š‹öAĒבžčéošŪWc|ZŹŪŪļ¢2sŁ…©øŗß”„?ā¬L2ˆe²ægvŪ Ēū%hźGĖ«„ž“¶‡{x„II0CļŅm®ó–ä}pyÆUØĪ+MĻ|ČČ|1MļĄĀƘ{4„fF÷Ė™÷Õ¾ZĒ­ń;ˆCq ėžoi­g½åÕdę“+Ÿ?įOµÉü Œć©Ń恑×Qįøn\ēĮHļo"¬Võ3"Š‚{*…āŹjŖ·V±„z#¶uq(–4ŠK2Āž.1ŁÉM¦¹®=čĪŃŪ=±yTeĢ-ÄF™Ė(L‡±ß_N±œöī&3‹©Ź˜¾ćsįæy“”££k„©”‡źüįļøFÓ520_œ“ĆøÕx’ÓęīGˆIßĖ:Kp·‰Łnŗęõ­ć‹Öų‚Gdg[q÷wėžwj­g=åż#„óIC¬…ńT½ó#Ū×h”Œ»Ž Ēuc$œ‹öīa×¾ ĖoéżM„ĶŖ¾#ŅŁųĻ5zæV“ēQŖcyīB×ŖJ“¢Y˜zŽ£&ˆŪͰæFG铔ķ<„õfŻÄRø”˜S×®ÜEļƽĘķ¬Ē)Ū÷±Mvśś¦R“ČLĢ Ć\Ļ 'ęAK9®Ü¦'把ż{1×µ3“ĻśR OCR ć—h?ś$„»¶P;ż ]#l(©ęĄv -=n¬ E”ĘćvĀā'+j£*N¢rwqpGĶw£É((%/zˆ¶3^Ūed¤”Ø‚jaSZŻ&U„ÆgĢkŪ©Ī~ĘŌ2JwW0qˁKŽŽĪŁß\Ž%źEk|Q‰Ū8²7™”ŽvzĘp™“ČŽPHŖ»›š;“ĢoHĶyjĪeĖ+XbINĻa]z,£]§8Ż>7ҚWzāÓZ/ZŚCO?Қ/zūGlS;#1¹”­_|ĘÖÓ/Q «Ķs{ ¶ ćmĶ>od’5z 8– §æii_£ćs÷÷3l.#3łĻöE×é™z’ņ„ ŗŗĢ<Ųčz^d‰~¤§¼ZźOs{(pćb'™lą‡ćimé`Ąi%9»³WĪz¾ 1\ē-Éūąā›įo|ÕłĆßqAßyߟ€ēCKŠZŽ©ī‹t;Ÿ$Æz/Ū“ŚˆŹ¤° ·~ ĶČyŽŃżŅšüÓ8¾hOļ<œ³‹Ūķ“t+¤—n 0qˆŽ‹śŸć¦¹žu”ü·oH擺×ĘS½ē##Ū×h!‚¼Žķ×zDņypFvf>¹1l¦›Ų§¼÷éżM„ßŖ^ˆ\˜æQ±Xģ<÷VżŖZ„ŒJÜNQę}g ‹ūüļR¹§”āŹjŠ'cƒ\;{™[£ąņĻ1ŗćE¹›Č-‡Éa7£ƒ=Ō5té.Ǥ«Ž '-Üw_)„»21õ÷ŅųĪMFļßAś2_ 3£öb'Éląpõެ ÷ŅK\0¢2g+;s ·Óxį&1U‡°TZPM4½ÓIüęrŖŠ,(cĪ^¤¦wn!-³āVĢ<ģYAQāŲų@&)µŸž¤ĘkŸīńZ.½›Ą–6vģó|žiąĘ ¼qĆó©`˱°^“Ę79t‰¶”CäęŚČ°Y‰v9lēŚŁĖ4ŒĢ5¤®<ĶĀV¹Åådt¬—†÷nPÓčX“™–¼ŅŸÖz™įÆ=@{?Ņ’/zūĒÖmnĶDqōŅxį ń»v@żĶkėąīö ²b†imp°š†ųPō_#Ē”`ó`!=żMKūŸ{¼–—ÄĮ¹»ˆū;œØåōvz·Ńć½/ ū‘žņj©?=ķ1Ś{ŠSo”SQYJqE>ÅÉČX/­7ū¼¶ ×yKņ>šųfhŸCqžšw\½ē}‚-ڟŽņ֜ÆĒz_…•)d ¶Ó|ź2ńö1’»œžē€Įż2ł§e|Ń3Ņ3h<׉us9UÅf”1g/RÓėØZēZĒSšß¾!™Oč^OõœŒj_£­Ōxčuhk_½"õ<苯ŲFzį§ŖŃŃ!¾§Z,)’ĄcģJiāŁēƄ;” [ŽżO'öĘó¼vmén%$>ĢC{ƹžĢIn…÷ńQe­ä©Ń“ęK¤×ßZéæF“ń 8‘^’÷¾Ez»­uk5Æ“ZėēK#D™mģłpęŚēyŪ¾ŗņ$źo%ÉxœHĻißČ"ķ±z¬ī;"׈؎V®ÖŻ wA ¦1i{HUFčīeę#‡į§²Ŗn­ ±µ’§”³|¾Dzż­½žk4‚™õ'yļOd¶ŪZvoäÕņÖņłŅ(Jl Š“į‘Õ—'‘Pį!ći VO¾HūFiH' ‘ ł¦¾ņD*=åØ>²ŸØžvĻó5b2)Ų’‡©§†ó}«k2u/Y+y.‘^ŅŽHņ^„›äÕbké|i$KB9iPĶd”eļīįåö՗'‘޾"²H¾±6ÉB¤‹~‡[n%¹6+øGģŖå¹s·Ć–Béæā^$y/BAņJheĶ®dGU®ń.\ sD”³qßa²-Sø'—~¦Ū”©Ÿ»īuš¶;sāŚ’æʑvóMk½¬•ņ ”…<#R!„B!„Br¾¾äH!„B!„B! % ‘B!„B!„Bˆ“…H!„B!„B!DČÉB¤B!„B!„"äd!R!„B!„Br²)„B!„B!„9YˆB!„B!„B„œ,D !„B!„B!BN"…B!„B!„!' ‘B!„B!„Bˆ“…H!„B!„B!DČÉB¤B!„B!„"äd!R!„B!„Br²)„B!„B!„9YˆB!„B!„B„œ,D !„B!„B!BN"…B!„B!„!' ‘B!„B!„Bˆ“…H!„B!„B!DČÉB¤B!„B!„"äd!R!„B!„Br²)„B!„B!„9YˆB!„B!„B„\D.DĘM=Äē’óūüĶ/g‡;!"ZåĒžļżą³ģf*Ü”¬ˆ{­¼+)ļĄŸņÆĻ|…¦Lj~O¼óżüįĄ_~(>„‘ !„˜aµīącš÷|ū’~Ÿļ’ßłĮW„;$!„B]ĢįĄ'UEAQŌpG"īa „ļēż{łį杊Üć* Š (!-ü4”7”ķ=^ĶÆ~÷‹ģ¼õg<żwW ß°‚ŠOQP…ČwĆÕ/׋iļūģ/ńŹlbcFč½q†žŸå|_ԊķÆņc’Ą—?†éŚßóԟ i|bm‹„|‡ķŸüM>`ėąōžˆK7 Ž ł1w׏8¾VŹ!„B¬“ˆ¼#RˆHRuˆĒž8ČaĖŹŽ}§ēø×žķ·łų/üļ(÷FWÖRŽP¶[Ź”³s]§~vÉš}a„ć±¾ČמŪ/šł’Y‘ćAųśåZšČ’ų"ŻCĖłń“—PK>Čļ~ķWŁążōī/6įqŽæ/óą­,žK‚Ńń‰µ-ł|šĮ·žõŸł—’ógüŽū‹ *‰vюMlŖŒeģĘšæ~~’³gĻröŚŻ×čqw­Œćk„B!ÄJ»7V/„Bø®åŚ;8”ØæƒĒ*0Ū_ę’6Xß¹"=>#I’Š'½ü3<¾iŠĘ’ųSžüŪ/š_ß’¾ń÷—N;ĀSGWdŪ?õ8ļ¼I­kń"¤Ńń‰µ-ł\öų7xz['/üż_šĶﵑüäēł­Ķ+»„DÅc¶‚{tåžųću|ƒĒݵ2ŽÆ•r!„+eÅ?š—öOżęcģ°å’Ź0=­ļńŹ?‡ēė'–}_tņC|ękæJŁŲ³üĆ=Ƶ±{įs؋e<ÅÆ|ęócq·ßąÄ?_"ł÷~ §¾ĀļüSĆģvÖóRęļ/ †ļ`Æ{‘Æ’ĆéŁmÖŻ’5¾ńYxłO^ĮņŲ“ģ³e`žŗĆÕgžżbÓģvQ±›xų©ĒŁs_łIÉÄ+#>ćĖ=üēüłoŒņÖ7ÉüŠA6˜q÷ÖsįßāŸĪ:tǧEōx5æüÆŸēžø)L¦(gŸųךkSž‹Ö>Ɨ~0ąU–G>ž Ż]@f8Śķ¼÷£äĻöčŠOĻqcīVóŃļž#)žßM*ēłÖĒžžs >«¬µ=@{^i„'_~ņōł±Ć3ETņńü!ū’Œ§’ņŖęņ†ŖŻęK-ų ‡J†ØłĖ7*/@ÜŲüöĻ>EĪOæĄ’¬’O}|;¶,3JļżĖWų꣱¹ćU|ˆ=u?›J҉ĆÅPóuĪ>÷łž……‡×Ÿæń „ą~ó÷ޱ#7×p=ēüm¾óvÆ×6÷’ī÷łä޹gIŽłį§łĀs‹/ŒõŌóråÕŪ¾FkIž÷:’£¬ÉIDAT?YdzovžšöŸ}‹w‡·±oļ}šŅɐī/¹čÓ|øŗė*¹{µæp¶Æ–~éÆæÅ=Ą§ŸłėO~™ßžGļń5ļĮÆóõßpńņo~ļu[ē ć‹Öó/h?/2ž+ł\P™Į¾Ä '€›Dmż.«Ź…«A•EĖųœyšOųŪO•`2™<-Ś’§üŪĻų§Ų’‘ż±¾ņ.Į”ĀqŚ^ś[žšūõš÷ŚęMr>B!‚·¢ ‘1¦}|āO?Éīø[œzłUźóŲżšż<õÕ ĢŸū ?ķņŽ5~/ŸžźÓlžæž£s}Ģž³Pˆ‹œOķqʆßćĶļ]¢'eļū§ˆ111ļŽÖ@ėy)V×&>ś•Oq$ī o>ū"MĆV2 7±aO5Փœœ÷1Y“;Ÿūż0~ų-žņ?b(ūĄÓ|ģ—‡_¾ń[üK£ē.­øäųążét_:Ļ·[艬cóĮyź«9˜æō%~Śź‰O1™°NVšŠÓ1¼żÆĖ×£X’§łŲ’óU”¾ßåŪuŠīųü™ˆ©åłožē,*ėö~‚_~ČĮ’ļywRÅÅXūšģ¶–ØM|ōæĄ#™mœ{õ?x¾3š¢m±’w’ˆ$õ÷ųėwĘ5ǧēøi—łł·žŠw-*9ļū >~Ÿ EUĮĒG5µ‡Ę¼ŅŹČöŠZŽP“ŪB[?¶““»/ń™‹Ž_ęHyć6Š’¾o”šæĆ+=.RŠ "=š,DfU~Ž’ń‡ŪHźØįä3ÆŅč¶’cŪĖĮGŖųž…ŗāÓ3D¹J8ų;ėčŗōĻžL"’ž#ž­/Żūū|ėš{v»ŚŸüõ¦+ū9ž0łų»žzöW^=ķktž­ł…q˜m¼<:÷¼»ń„ “7+XrJŲģ:ĮU‹ö?šéŻßĆæ¾Ų×’×Ū³ųÅ öĪöÕŅ/µō·ŃŲ’æ½;«ŖĪ8ž9—{Łe„\Q'Q@įŗÆYĪŲ3YfNMĖ“Õ<->ӞĶ3MS>3M:£=–i–™Mć’I&j⮉ īŠ€ȾŻóül‚p^ø.ß·/^÷Üßłž~æļłż~÷ÜsĻŻĀ¤Ēlžˆ¼+Ŗ;†s’®= ٚńŖ¹ńEėü«u^ŠŸsn3cž%Ā9™of¼EtIėīæh‹|ĪJ»ŒOŲhśnŚH¦ŪH†°³4«v{{ĻŽų“š­}æZĒēü„„üłŻp-ŗ‹1oŻKŸcĖ™æīg4ļÆ6V+ÆK“äU[Ģӊŕž}JłfÅNĀĮ#sü8¶ń_¬É¼—ī™Ź„µļ[l§+O[Z7É|$„B\æv=éĻż„yg±ķĶ?²(¹z”¶ckÆ.x õgķ‡Ē=ĒŁŹCļ>Å 5–ßYĪ‘źmx ÷ŌIō3¦°śOļ±ś‚ˆ#)ū=ęüĪ•ś×1“¦›cņLæ2Ž/xÅ[kŚ~,;®>)œ÷Ÿl«^ˆ›·Ž Į3Ń NW_5Pxį#ž›Łp;6äšÜ§33±'k?IkšXśęæńÉÖ\ĪĻ_B×~o0ņįa|2'¾Uńµ$#é €ÆbT„ˆ3‡rزńb±Ū¤ĒēŸĘ†Wę°<ķŹĒ³ā¢I›³ŒG¦Ż»VéŠOė~UŌŚm+TBpóõi©?“ę•VÖī­õµvæÕēźöćB,œYc•śŗv/gåģ’!&ćŹ¼w??^yĢ”$ˆ{ž Å+3†w’°‚#•5/j×±ümŌ\|zĘEµ§ģąŸxkaõɇļO`XōagįkqµŪ]>›Ä”³ąRīO”2 ·&bŅŚĪZ뫵­·ŠN@)Ź¢kUś½ö6’įĒ‚W‘WTÅĆO;}ßx„§<’!o0¦ŪV¼šrÅēŗŹ³U’jĶS­ĒŪŃĶ')})˜Gz-ęó+·Rp¶»›Įw©d~[w„T«Ę«fĘ­óÆÖy”5ńYƒ-ņyēŖÅĽł,o/™j($-žcńaąš±ŒB÷ó$,]ČgŁŚ?6­5_* ΐ| ķ ©€Ź¼d<­«Ķź³öŗDk^µÅ<}jćb6©äŠä©ž ¬YĖ©ĪE®•b§;O[Z7É|$„B\Ÿv}+®w°†‹X”¬ ^łW\š-ń'Üś…4~‚SS’ü{F;'°äĶ„$äß¾'!ƒ=°KŻĪź ĘŚöĖŽŪIźU—#énēņŖ(Tčü‹Śņź’«Ļb—ĶÉ}9µUåd‘]>k·i²ŒĀÓdä*øūv¾Ŗ¼,’w^ØŻ®Ü”HRb1¦ž!„©Ŗīų“hü¼¦Ė 6ūaLŻĀņ4KƒĒmĻ@õ¾“ńv]ńiŻoÓŪ6MKhĶ+­¬Ż ­¾Öī·śzLA·Ņ=ÄlŹoōXkź[•ü1Ę&·5FģUNŹw«9R©h*ƹųōŒ»\R6&ÖnWfŲ˾%{c®jœ«ĶõˆÖvÖZ_­żŪłw³sČļŃ^…Š ,…wŠ«Æ|šwV©*‹ƒ {M£§<‡ŖPī}¬«?'¾ é÷>õ”g«žÕš§Z·Ė;wr¬Ä—ĆŗÖžĶ9j½ éüüÓŁŚæµf¼jn|i²Ķš˜µĪ zć+ńÜAʾżģßs”4Kėę[åsUÉA¾zķ žœż. >YĻ1—)¼ōī3Œź—ÉŽĻ_ē…’ʲĆÅŗź¢5_šŹšėÉ{kÆK“ę•Öņ“ę•j($?·•’‚J —Š8lRP³r)RMø»u•śÖ±-ÕCę#!„¢iķzE¤§(yé/s* Ą‡0UeO½[v6_E*zwRŲVpūNŚŽ…”x»ŖŸĪlŠ~–œlņŹ”c½mõ“³‚‚GēNŲY®¼HQŹÉ¹x¹Į¾ +VšżÖž2æĀKs8{ā$gS²żūm$]lų1P”2ņ/םßVÕ’½źUAąÄgx`üŗy»`ÆŖÕ÷Ł1X°Ėnų%»\.ęŌ+•˗бøxāėl;]ńi©ÆŽ…”xuRQgņł4xĢ`gDµÆĄµ“2 śŚĻŚZč=ym/ķLOæÕp®˽QīäĒŰ«“ń ­©oAję5_„8śxÓQ)āHj>*-Ö©„ųōŒŖ!—ó¹ĘŪådęƒÉŸŽUÆmŹŠÓĪzėŪ’9’l„Üż(•å ˜LœwßĒņ?¾Ļ.§S*¶`vCYå:Ī é)ÆūƒfĻĀčlŌk¼÷©§<[õÆÖ<Õz¼•(ŁstO›aYõÕXwEöĘéÜ:–§9j«Ę+h~|–ē_­óBkāSQŁ4ļolŖļÅØ]g«|6: `Üc“>°3eĒ÷±-v>qEc˜ńģTf…OäĢμ½xŸ®ŗč][K[GZ×u-ќW5ŸŠ/­nŸr„°^[šĄ¾Ē‘Ęu¬2 !„M3†NÆ~ēżČŖ;m øź£4¹ūXśž^ś¼ś Ć?ų—¾āx+ßAæłYļāÕśķģR>‘YógВʉ%‹]_{¶ö£bP½(޹ą9RbĶD @æĄ@"' a„9”Å/ĪeK‘¾{<õœ<—Ł{pń§h–ÆL'«Ø» o†ĻzŠaWm«ØÆlØrlų»žų“ŌW›źž(Nü’֜müpE1‰Œ Xæż¬K_^Ł"_¬K{æÕč2eŒÉ|żķ)Ō&Ž»iM}Ė+*¬V£–ākI£q· Ŗ¢7^żķl-7vžŁ†ŠŹÅP½¼QQ):›ĄA<\LņrÉ«T@ć0ص<'ćīū„c·p¶£7T°w÷ĄPM.ųurćüÅĖŗā»Łū·ęxSQIŚ|Šā’ÄōĪ_±&ón"M\Ų²­Ž‰”ÖGĶ/Śę_­óBėā»Ž+Įl•Ļ–ŖrÆęՏ’ź7ž]‰Ż‹™ŸäĖøß>Ė_~łÆFē^WżźÓ2>·†-×u-ӖW®Ķ¤©jWA95-'ó‘Bq£i×+"ss@ķāßčļ^^&(Éb×U‹ßŒ}_±ńģꦒŪxź7ńĢ^–Ž^įŽPJ]ö’]ØąäćŻąļ†Žšøj±­§ Mß³jĮe¶\y]  ¹aj€—NijžD<뿁ąõ×BīĢ–8}wŒˆź†)q//ŲYū7§g†vP”į—ó¢X<éā[ŁąJ¬Īīī “U¬Ō-5ʧ¹¾õŲ«_4•ŗģ%'GĮŁ„ŒÄ#‰M?Qių’zŪÆ©żZ›ž¼ėę‹RV˜pvØ«§ź£«uźf~³/ bÜŻ](Ū÷їšy”bÅć£43›\µ'~wøAJóĻÕŸžń@±xāēQ õ¾Ą¦£;Jł²r šĻOčig=õ­ÆŁćÊżq«HO-ĘŅėĘ9W±ńŹr8†ā×]„āÜiėųb­å¹ŗūąé`¢ė=s˜7©ī¹ŠAEķósē?ĢĻóįƒ]F}ńŁ µę©žć­`Ū^NĢz˜C}‰K £·K:;L„ę šīyF-óÆÖy”-āÓŹłLÅqvo£¶NŽ”ƒé—ĆĢ/wźĢ_ļƒčXĶūÕ».¶*+®Kō¬ėZ*ĻŚyÕy*ó‘B”O»Ž#ņä” ŌNĮ<Ž·nĀvrŒ¹·JĮ±Ŗ d'}ČŅeų}ŽG{“gÄ7’’ó°tāŗoÆuL7KƐ®vV 9~;Ū·W’ģŲvS%×X‘)u?§²(PLŽśO–Ł;QŪ·5?ī‘c qi\–”Ź“¾cÖžīX>”`#•g/еħ£¾å9(Ŗ ĪšØ£ū÷gP0žĒūšģ:x7ńõŪÆŁżZ›Ž¼ŖŁŽZłRš—G‘Ňn½ź>ęčdJéśźmĶ~ó1…!3‰Žßņ +•É;Iŗä@·É÷ŃĒŠüóµÄ§g<Ø>Žk·Æ %t–扜¾ŻSG;ė©/č8>¬Ō·ŠŒĶÉŪõaäšŗ“Ka£q-#}wB“ł3įłŁ¼ōāż rh|B-åęÆ`ŁēĆē1o^õĻĀ~$„BAIßČ’}ų1ɆÖÅ×Īż«5OõoEĘļųłd%Ż¢Ę3jd?œ/ąÓŌzgśZ3Ļ“@Óü«u^he|Ķå•V¶ČēŚŸŠ‚B][*%*čüµīu±µYi]¢g]×byÖĪū68Ž4ÕćŖd>B!ź“ė‘é1«Ł;~£_x CōNNXü‰˜8’žv)¬Y™Hõż‚š @āĀ/Ł3’i&üžAvĢ^y[~Dūäņ ś¼ń"Öķ'Ėk‡{QqUS“ŗÆĮ­ūüq¶?ē÷į`ZUJB&Äæ*‘/w•č.ļąĮ|&Œ’-o=éĮ¶ćeøų…3:ʇ²‚Ę}Ŗ q~Ž9Oʰķ”żFŒ%Ā=“ŻóvŌ.Š­_¢ä“dWeģS’{’¼ …’ŌC½r_Ÿóß|Į¦!Ļ3ö÷šÜ¼‘C§«°óėFļžA rYĻcÆĒ¶*¾–öėģH€Oõsŗt1¢X< ¢ČjU6G’2uÕSk^i„µ¾åGö“XAÄ£Æóx·\P~ADä”( &½õµV昈ńä§,9aŗę Bkē_™ćÖ~t€_¼:™’žėĶÖ͇Č(tĘ÷®Ā;üĄ½Æ+>=な”ćüo=C|Ё€±S0wĪd÷¢­ŗ_kmg=õ…–ū·­Ęƒ›]Öń¹|{źs¦?ō:čųI%=61„—bYšże®ī`§ÜHBBčģĢńTZSžGwļmXn‘Ā ē' ä§æg/5ļ‡jĻVż«5OuĶæ $nN£ģé Lō6‘»5¾Ńq¦g¼ŅBėü«u^Š_Ky„•Mņł*ÅG÷‘ŅńW¼|o1Ē}=³'™ū>ÖUkÆ×“²öŗDĻŗNKyÖĪ{k—§µ2 !„M³ 4æż6@ÖѶłĪ*-©Ł‹Sæ £̄č‚]NėꒃµgźnŚlR{0×ćˆM(Ŗ}njZCīĮ@§~8d½{šÜ,*Ź“I9ąÄCĀJĻ\ö/ŚLž9„NiXææśKC“¶³VUŖ·^wŅ'h a‘‘ ¹Ė愯ÄĢ[Äwéuå¹tĶ„pHŽž‘£eÕ/.„ŽŻ?œ®yۈŽ^}gń bČõ $x搾ųŗ¦·hGrgŁ.žżSõĒļŻzcLx.[ęnA5™q#ńVΰė“y,ÜwYw|ŗŪ»(sÅō 63j„™ČČHzXāŲtøōŹ~3HܚD‘WB†e¾‹_ģņ²9v ÉéE­ŠÆ„żöžņÆĶ‡ŁlfPG /z7c6›‰źĢ¹5{9§(šūCk^i„µ¾•v©œ:Ӂīƒ2®.éü4…‘Cħµ¾Öī7®Ļ0óĮŽœśxŪ3®}厞ž5Uö lś`:$Ē{øššedn'ᨊWßĀ#Ģ„÷ĀÓ”ĪŽļ6rų\©®ų“ŽnŻG3&ź[ęnĮ5™ ć‚čĀ•ćmoÓ9`_Շ”Óāp8š) ļK§µµÖ·FĖżŪ6ćĮ­ąĢ®ć”ł÷'4jCŻ(:½™Ļžŗ’żEóǾ“Į‡ÓÕ>‡¤Õ›I©l|E¬žņjĖż™6”ī9uc½žņlŁæZņTļü[yމĄ_‡ąmŸÅ¾Oæ`_nĆzŽ#-ć‹ÖłWė¼ '>Š–WZŁ:Ÿ++Orś¤?įÓ§2etŌ#_šŹā$]uŠ›/ĘrMJ— qÄģiż:ŲŚė­y„µ<-yeÆō#jj_J·F³+KĮ7ōW˜½YõƉ+Æ#‚Pv®f{ŗAsžj]7iƇĢGB!DS”ū^©¾±Éše5BæEćxś»Ēšśv/ykœõó>ļĶ*ę›ßĢ!ŗø]ļ"pŪ»•óJ«‘//ćɞ±L’϶„I7B|ĪĘI¼øā78}öÆÆæö‰U!ÄĶOę!„Ba rvē&ē4^Ź%ŅēŲ:”¶Ó_Ś"ŗ-ņŖĘŹŽT¤Å±āć[‡Ņ¤%>‡NŻńUĖČĻͳiBˆ¶w»Ļ B!„Ā:Śõ‘āś8…2}ĮDģīāš‰KœƒłPN'—óCrNY“ŽäUc•Ęl_qĀÖa\“-ćsńaP';‚&EąYšČš#·ß={…ø•ɼ „B!ڊœˆ¼‰”ŗģētŚīŽz€°»QJ.’–ō|¼ŽÖ”‰›˜ä•ŠĆsČTfMķ @Q^2?.\Ȧ¹į¾·™„B!D[‘{D !„B!„B!ڜ|¾F!„B!„BŃęäD¤B!„B!„¢ĶɉH!lĄ±h0³>_Į?ž°u(B!„B!„ķB¾¬¦žésW2¹«„ö÷ņ¢<ΟŚĆęå_ńCjYķßxg%S¼Öó—ß-ć°}Ż·ÅŽóĪJ¦wŁČßf,ęg…Gē­d‚O)§W>Ėėk k· }n /DäŸÓžĪ.Å6炇<¹„G96æŃ™OyčÕMmƒkĄ$& ½Ģæ–Ē7æņ+ž’zdņӛO³(Å”Ķbj7Ŗ‚¢((ŠZū'[ę‹Ū“xčŃ1 ģė‡'…ädqöč6|½Ž„‹ņ~…B!„B!®Ÿœˆ¼ŠZšĀ–U?“‰+ī]zjžĄ£ļvĒeö›¬¹ æ¹՞ž£&ĀŚ·A“­w~ĻZ¾½ģ €bq!`äXś;b÷¦ĆdVVo£dœnÓ<‚F0ł¾<²æž‰Ķ×>Łå6ˆ€ņL.åłpēŠn’٦qŁ’-ņÅ£ó£¼śē‰ų”„p`㷜Ŗ4q‡ośFLaü®h.¶[(B!„B!„ø…ż? ZIŹ–>–¶IEND®B`‚go-snaps-0.5.16/internal/000077500000000000000000000000001511036776100151555ustar00rootroot00000000000000go-snaps-0.5.16/internal/colors/000077500000000000000000000000001511036776100164565ustar00rootroot00000000000000go-snaps-0.5.16/internal/colors/colors.go000066400000000000000000000056321511036776100203140ustar00rootroot00000000000000package colors import ( "fmt" "io" "os" "strings" "github.com/gkampitakis/ciinfo" ) const ( reset = "\x1b[0m" RedBg = "\x1b[48;5;225m" GreenBG = "\x1b[48;5;159m" BoldGreenBG = "\x1b[48;5;23m" BoldRedBg = "\x1b[48;5;127m" BoldWhite = "\x1b[1;38;5;255m" Dim = "\x1b[2m" Greendiff = "\x1b[38;5;22m" Reddiff = "\x1b[38;5;52m" Yellow = "\x1b[33;1m" White = "\x1b[38;5;255m" Green = "\x1b[32;1m" Red = "\x1b[31;1m" ) var NOCOLOR = isNoColor() func isNoColor() bool { // https://no-color.org (with any value) _, noColor := os.LookupEnv("NO_COLOR") termenv, isTerm := os.LookupEnv("TERM") return noColor || (!isTerm && !ciinfo.IsCI) || termenv == "dumb" } func Sprint(color, s string) string { if NOCOLOR { return s } return fmt.Sprintf("%s%s%s", color, s, reset) } func Fprint(w io.Writer, color, s string) { if NOCOLOR { io.WriteString(w, s) return } fmt.Fprintf(w, "%s%s%s", color, s, reset) } /** Only used for the pretty_diff */ func FprintEqual(w io.Writer, s string) { if NOCOLOR { fmt.Fprintf(w, " %s", s) return } // we use the space here for aligning with insert and delete sign fmt.Fprintf(w, " %s%s%s", Dim, s, reset) } func FprintDelete(w io.Writer, s string) { if NOCOLOR { fmt.Fprintf(w, "- %s", s) return } // this is for mitigating https://unix.stackexchange.com/q/212933 // couldn't find a better way. if hasNewlineSuffix(s) { fmt.Fprintf(w, "%s%s- %s%s\n", Reddiff, RedBg, trimSuffix(s), reset) } else { fmt.Fprintf(w, "%s%s- %s%s", Reddiff, RedBg, s, reset) } } func FprintInsert(w io.Writer, s string) { if NOCOLOR { fmt.Fprintf(w, "+ %s", s) return } if hasNewlineSuffix(s) { fmt.Fprintf(w, "%s%s+ %s%s\n", Greendiff, GreenBG, trimSuffix(s), reset) } else { fmt.Fprintf(w, "%s%s+ %s%s", Greendiff, GreenBG, s, reset) } } func FprintDeleteBold(w io.Writer, s string) { if NOCOLOR { io.WriteString(w, s) return } fmt.Fprintf(w, "%s%s%s%s", BoldRedBg, White, s, reset) } func FprintInsertBold(w io.Writer, s string) { if NOCOLOR { io.WriteString(w, s) return } fmt.Fprintf(w, "%s%s%s%s", BoldGreenBG, White, s, reset) } func FprintRange(w io.Writer, r1, r2 string) { if NOCOLOR { fmt.Fprintf(w, "@@ -%s +%s @@\n\n", r1, r2) return } fmt.Fprintf(w, "%s@@ -%s +%s @@%s\n\n", Yellow, r1, r2, reset) } func FprintBg(w io.Writer, bgColor, color, s string) { if NOCOLOR { io.WriteString(w, s) return } if hasNewlineSuffix(s) { fmt.Fprintf(w, "%s%s%s%s\n", bgColor, color, trimSuffix(s), reset) } else { fmt.Fprintf(w, "%s%s%s%s", bgColor, color, s, reset) } } // hasNewlineSuffix checks if the string contains a "\n" at the end. func hasNewlineSuffix(s string) bool { return strings.HasSuffix(s, "\n") } // trimSuffix is like strings.TrimSuffix but specific for "\n" char and without checking // it exists as we already have earlier. func trimSuffix(s string) string { return s[:len(s)-1] } go-snaps-0.5.16/internal/colors/colors_test.go000066400000000000000000000125351511036776100213530ustar00rootroot00000000000000package colors import ( "io" "strings" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestPrintColors(t *testing.T) { t.Run("string utils", func(t *testing.T) { t.Run("hasNewlineSuffix", func(t *testing.T) { test.False(t, hasNewlineSuffix("hello \n world")) test.True(t, hasNewlineSuffix("hello \n world\n")) }) t.Run("trimSuffix", func(t *testing.T) { test.Equal(t, "hello \n world\n", trimSuffix("hello \n world\n\n")) }) }) t.Run("no color", func(t *testing.T) { t.Cleanup(func() { NOCOLOR = false }) NOCOLOR = true t.Run("[Sprint] should return text as is", func(t *testing.T) { test.Equal(t, "hello world", Sprint(Dim, "hello world")) }) for _, v := range []struct { name string expected string formatter func(w io.Writer) }{ { name: "[Fprint] should print string as is", expected: "hello \n world", formatter: func(w io.Writer) { Fprint(w, Dim, "hello \n world") }, }, { name: "[FprintEqual] should print string with space", expected: " hello \n world", formatter: func(w io.Writer) { FprintEqual(w, "hello \n world") }, }, { name: "[FprintDelete] should print string with - sign", expected: "- hello \n world\n", formatter: func(w io.Writer) { FprintDelete(w, "hello \n world\n") }, }, { name: "[FprintInsert] should print string with + sign", expected: "+ hello \n world\n", formatter: func(w io.Writer) { FprintInsert(w, "hello \n world\n") }, }, { name: "[FprintBg] should print string as is", expected: "hello \n world\n", formatter: func(w io.Writer) { FprintBg(w, "bg_color", "color", "hello \n world\n") }, }, { name: "[FprintDeleteBold] should print string as is", expected: "hello \n world", formatter: func(w io.Writer) { FprintDeleteBold(w, "hello \n world") }, }, { name: "[FprintInsertBold] should print string as is", expected: "hello \n world", formatter: func(w io.Writer) { FprintInsertBold(w, "hello \n world") }, }, { name: "[FprintRange] should return range format", expected: "@@ -1 +2 @@\n\n", formatter: func(w io.Writer) { FprintRange(w, "1", "2") }, }, } { v := v t.Run(v.name, func(t *testing.T) { t.Parallel() var s strings.Builder v.formatter(&s) test.Equal(t, v.expected, s.String()) }) } }) t.Run("with color", func(t *testing.T) { t.Run("[Sprint] should return color wrapped text", func(t *testing.T) { test.Equal(t, "\x1b[2mhello world\x1b[0m", Sprint(Dim, "hello world")) }) for _, v := range []struct { name string expected string formatter func(w io.Writer) }{ { name: "[Fprint] should print string as is", expected: "\x1b[2mhello \n world\x1b[0m", formatter: func(w io.Writer) { Fprint(w, Dim, "hello \n world") }, }, { name: "[FprintEqual] should print string with dim and space", expected: " \x1b[2mhello \n world\x1b[0m", formatter: func(w io.Writer) { FprintEqual(w, "hello \n world") }, }, { name: "[FprintDelete] should return red colored text and escape suffix newline", expected: "\x1b[38;5;52m\x1b[48;5;225m- hello \n world\x1b[0m\n", formatter: func(w io.Writer) { FprintDelete(w, "hello \n world\n") }, }, { name: "[FprintDelete] should print colored string as is", expected: "\x1b[38;5;52m\x1b[48;5;225m- hello \n world\x1b[0m", formatter: func(w io.Writer) { FprintDelete(w, "hello \n world") }, }, { name: "[FprintInsert] should return green colored text and escape suffix newline", expected: "\x1b[38;5;22m\x1b[48;5;159m+ hello \n world\x1b[0m\n", formatter: func(w io.Writer) { FprintInsert(w, "hello \n world\n") }, }, { name: "[FprintInsert] should print colored string as is", expected: "\x1b[38;5;22m\x1b[48;5;159m+ hello \n world\x1b[0m", formatter: func(w io.Writer) { FprintInsert(w, "hello \n world") }, }, { name: "[FprintBg] should return green colored text and escape suffix newline", expected: "bg_colorcolorhello \n world\x1b[0m\n", formatter: func(w io.Writer) { FprintBg(w, "bg_color", "color", "hello \n world\n") }, }, { name: "[FprintBg] should print colored string as is", expected: "bg_colorcolorhello \n world\x1b[0m", formatter: func(w io.Writer) { FprintBg(w, "bg_color", "color", "hello \n world") }, }, { name: "[FprintDeleteBold] should apply red bold coloring", expected: "\x1b[48;5;127m\x1b[38;5;255mhello \n world\x1b[0m", formatter: func(w io.Writer) { FprintDeleteBold(w, "hello \n world") }, }, { name: "[FprintInsertBold] should apply green bold coloring", expected: "\x1b[48;5;23m\x1b[38;5;255mhello \n world\x1b[0m", formatter: func(w io.Writer) { FprintInsertBold(w, "hello \n world") }, }, { name: "[FprintRange] should apply yellow color and format", expected: "\x1b[33;1m@@ -1 +2 @@\x1b[0m\n\n", formatter: func(w io.Writer) { FprintRange(w, "1", "2") }, }, } { v := v t.Run(v.name, func(t *testing.T) { t.Parallel() var s strings.Builder v.formatter(&s) test.Equal(t, v.expected, s.String()) }) } }) } go-snaps-0.5.16/internal/difflib/000077500000000000000000000000001511036776100165545ustar00rootroot00000000000000go-snaps-0.5.16/internal/difflib/difflib.go000066400000000000000000000361561511036776100205150ustar00rootroot00000000000000package difflib /* This package is a partial port of Python difflib. This library is vendored and modified to address the `go-snaps` needs for a more readable difference report. Original source: https://github.com/pmezard/go-difflib Copyright (c) 2013, Patrick Mezard All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import "strconv" const ( // Tag Codes OpEqual int8 = iota OpInsert OpDelete OpReplace ) type match struct { A int B int Size int } type OpCode struct { Tag int8 I1 int I2 int J1 int J2 int } func min(a, b int) int { if a < b { return a } return b } func max(a, b int) int { if a > b { return a } return b } // FormatRangeUnified converts range to the "ed" format. func FormatRangeUnified(start, stop int) string { // Per the diff spec at http://www.unix.org/single_unix_specification/ beginning := start + 1 // lines start numbering with one length := stop - start if length == 1 { return strconv.Itoa(beginning) } if length == 0 { beginning-- } return strconv.Itoa(beginning) + "," + strconv.Itoa(length) } // sequenceMatcher compares sequence of strings. The basic // algorithm predates, and is a little fancier than, an algorithm // published in the late 1980's by Ratcliff and Obershelp under the // hyperbolic name "gestalt pattern matching". The basic idea is to find // the longest contiguous matching subsequence that contains no "junk" // elements (R-O doesn't address junk). The same idea is then applied // recursively to the pieces of the sequences to the left and to the right // of the matching subsequence. This does not yield minimal edit // sequences, but does tend to yield matches that "look right" to people. // // sequenceMatcher tries to compute a "human-friendly diff" between two // sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the // longest *contiguous* & junk-free matching subsequence. That's what // catches peoples' eyes. The Windows(tm) windiff has another interesting // notion, pairing up elements that appear uniquely in each sequence. // That, and the method here, appear to yield more intuitive difference // reports than does diff. This method appears to be the least vulnerable // to synching up on blocks of "junk lines", though (like blank lines in // ordinary text files, or maybe "

" lines in HTML files). That may be // because this is the only method of the 3 that has a *concept* of // "junk" . // // Timing: Basic R-O is cubic time worst case and quadratic time expected // case. sequenceMatcher is quadratic time for the worst case and has // expected-case behavior dependent in a complicated way on how many // elements the sequences have in common; best case time is linear. type sequenceMatcher struct { a []string b []string b2j map[string][]int IsJunk func(string) bool autoJunk bool bJunk map[string]struct{} matchingBlocks []match fullBCount map[string]int bPopular map[string]struct{} opCodes []OpCode } func NewMatcher(a, b []string) *sequenceMatcher { m := sequenceMatcher{autoJunk: true} m.setSeqs(a, b) return &m } // Set two sequences to be compared. // // s.SetSeqs("abcd", "bcde") func (m *sequenceMatcher) setSeqs(a, b []string) { m.setSeq1(a) m.setSeq2(b) } // Set the first sequence to be compared. // // The second sequence to be compared is not changed. // // sequenceMatcher computes and caches detailed information about the // second sequence, so if you want to compare one sequence S against // many sequences, use m.setSeq2(S) once and call m.setSeq1(x) // repeatedly for each of the other sequences. // // See also setSeqs() and setSeq2(). func (m *sequenceMatcher) setSeq1(a []string) { if &a == &m.a { return } m.a = a m.matchingBlocks, m.opCodes = nil, nil } // Set the second sequence to be compared. // // The first sequence to be compared is not changed. // // sequenceMatcher computes and caches detailed information about the // second sequence, so if you want to compare one sequence S against // many sequences, use m.setSeq2(S) once and call m.setSeq1(x) // repeatedly for each of the other sequences. // See also setSeqs() and setSeq2(). func (m *sequenceMatcher) setSeq2(b []string) { if &b == &m.b { return } m.b = b m.matchingBlocks, m.opCodes, m.fullBCount = nil, nil, nil m.chainB() } func (m *sequenceMatcher) chainB() { // Populate line -> index mapping b2j := map[string][]int{} for i, elt := range m.b { indices := b2j[elt] indices = append(indices, i) b2j[elt] = indices } // Purge junk elements m.bJunk = map[string]struct{}{} if m.IsJunk != nil { junk := m.bJunk for elt := range b2j { if m.IsJunk(elt) { junk[elt] = struct{}{} } } for elt := range junk { // separate loop avoids separate list of keys delete(b2j, elt) } } // Purge popular elements that are not junk popular := map[string]struct{}{} n := len(m.b) if m.autoJunk && n >= 200 { ntest := n/100 + 1 for s, indices := range b2j { if len(indices) > ntest { popular[s] = struct{}{} } } for s := range popular { delete(b2j, s) } } m.bPopular = popular m.b2j = b2j } func (m *sequenceMatcher) isBJunk(s string) bool { _, ok := m.bJunk[s] return ok } // Find longest matching block in a[alo:ahi] and b[blo:bhi]. // // If IsJunk is not defined: // // Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where // alo <= i <= i+k <= ahi // blo <= j <= j+k <= bhi // and for all (i',j',k') meeting those conditions, // k >= k' // i <= i' // and if i == i', j <= j' // // In other words, of all maximal matching blocks, return one that // starts earliest in a, and of all those maximal matching blocks that // start earliest in a, return the one that starts earliest in b. // // If IsJunk is defined, first the longest matching block is // determined as above, but with the additional restriction that no // junk element appears in the block. Then that block is extended as // far as possible by matching (only) junk elements on both sides. So // the resulting block never matches on junk except as identical junk // happens to be adjacent to an "interesting" match. // // If no blocks match, return (alo, blo, 0). func (m *sequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) match { // CAUTION: stripping common prefix or suffix would be incorrect. // E.g., // ab // acab // Longest matching block is "ab", but if common prefix is // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so // strip, so ends up claiming that ab is changed to acab by // inserting "ca" in the middle. That's minimal but unintuitive: // "it's obvious" that someone inserted "ac" at the front. // Windiff ends up at the same place as diff, but by pairing up // the unique 'b's and then matching the first two 'a's. besti, bestj, bestsize := alo, blo, 0 // find longest junk-free match // during an iteration of the loop, j2len[j] = length of longest // junk-free match ending with a[i-1] and b[j] j2len := map[int]int{} for i := alo; i != ahi; i++ { // look at all instances of a[i] in b; note that because // b2j has no junk keys, the loop is skipped if a[i] is junk newj2len := map[int]int{} for _, j := range m.b2j[m.a[i]] { // a[i] matches b[j] if j < blo { continue } if j >= bhi { break } k := j2len[j-1] + 1 newj2len[j] = k if k > bestsize { besti, bestj, bestsize = i-k+1, j-k+1, k } } j2len = newj2len } // Extend the best by non-junk elements on each end. In particular, // "popular" non-junk elements aren't in b2j, which greatly speeds // the inner loop above, but also means "the best" match so far // doesn't contain any junk *or* popular non-junk elements. for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && m.a[besti-1] == m.b[bestj-1] { besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 } for besti+bestsize < ahi && bestj+bestsize < bhi && !m.isBJunk(m.b[bestj+bestsize]) && m.a[besti+bestsize] == m.b[bestj+bestsize] { bestsize++ } // Now that we have a wholly interesting match (albeit possibly // empty!), we may as well suck up the matching junk on each // side of it too. Can't think of a good reason not to, and it // saves post-processing the (possibly considerable) expense of // figuring out what to do with it. In the case of an empty // interesting match, this is clearly the right thing to do, // because no other kind of match is possible in the regions. for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && m.a[besti-1] == m.b[bestj-1] { besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 } for besti+bestsize < ahi && bestj+bestsize < bhi && m.isBJunk(m.b[bestj+bestsize]) && m.a[besti+bestsize] == m.b[bestj+bestsize] { bestsize++ } return match{A: besti, B: bestj, Size: bestsize} } // Return list of triples describing matching subsequences. // // Each triple is of the form (i, j, n), and means that // a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in // i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are // adjacent triples in the list, and the second is not the last triple in the // list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe // adjacent equal blocks. // // The last triple is a dummy, (len(a), len(b), 0), and is the only // triple with n==0. func (m *sequenceMatcher) getMatchingBlocks() []match { if m.matchingBlocks != nil { return m.matchingBlocks } var matchBlocks func(alo, ahi, blo, bhi int, matched []match) []match matchBlocks = func(alo, ahi, blo, bhi int, matched []match) []match { match := m.findLongestMatch(alo, ahi, blo, bhi) i, j, k := match.A, match.B, match.Size if match.Size > 0 { if alo < i && blo < j { matched = matchBlocks(alo, i, blo, j, matched) } matched = append(matched, match) if i+k < ahi && j+k < bhi { matched = matchBlocks(i+k, ahi, j+k, bhi, matched) } } return matched } matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) // It's possible that we have adjacent equal blocks in the // matching_blocks list now. nonAdjacent := []match{} i1, j1, k1 := 0, 0, 0 for _, b := range matched { // Is this block adjacent to i1, j1, k1? i2, j2, k2 := b.A, b.B, b.Size if i1+k1 == i2 && j1+k1 == j2 { // Yes, so collapse them -- this just increases the length of // the first block by the length of the second, and the first // block so lengthened remains the block to compare against. k1 += k2 } else { // Not adjacent. Remember the first block (k1==0 means it's // the dummy we started with), and make the second block the // new block to compare against. if k1 > 0 { nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) } i1, j1, k1 = i2, j2, k2 } } if k1 > 0 { nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) } nonAdjacent = append(nonAdjacent, match{len(m.a), len(m.b), 0}) m.matchingBlocks = nonAdjacent return m.matchingBlocks } // Return list of 5-tuples describing how to turn a into b. // // Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple // has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the // tuple preceding it, and likewise for j1 == the previous j2. // // The tags are characters, with these meanings: // // OpReplace (replace): a[i1:i2] should be replaced by b[j1:j2] // // OpDelete (delete): a[i1:i2] should be deleted, j1==j2 in this case. // // OpInsert (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. // // OpEqual (equal): a[i1:i2] == b[j1:j2] func (m *sequenceMatcher) getOpCodes() []OpCode { if m.opCodes != nil { return m.opCodes } i, j := 0, 0 matching := m.getMatchingBlocks() opCodes := make([]OpCode, 0, len(matching)) for _, m := range matching { // invariant: we've pumped out correct diffs to change // a[:i] into b[:j], and the next matching block is // a[ai:ai+size] == b[bj:bj+size]. So we need to pump // out a diff to change a[i:ai] into b[j:bj], pump out // the matching block, and move (i,j) beyond the match ai, bj, size := m.A, m.B, m.Size var tag int8 = 0 if i < ai && j < bj { tag = OpReplace } else if i < ai { tag = OpDelete } else if j < bj { tag = OpInsert } if tag > 0 { opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) } i, j = ai+size, bj+size // the list of matching blocks is terminated by a // sentinel with size 0 if size > 0 { opCodes = append(opCodes, OpCode{OpEqual, ai, i, bj, j}) } } m.opCodes = opCodes return m.opCodes } // Isolate change clusters by eliminating ranges with no changes. // // Return a generator of groups with up to n lines of context. // Each group is in the same format as returned by getOpCodes(). func (m *sequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { if n < 0 { n = 3 } codes := m.getOpCodes() if len(codes) == 0 { codes = []OpCode{{OpEqual, 0, 1, 0, 1}} } // Fixup leading and trailing groups if they show no changes. if codes[0].Tag == OpEqual { c := codes[0] i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} } if codes[len(codes)-1].Tag == OpEqual { c := codes[len(codes)-1] i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} } nn := n + n groups := [][]OpCode{} group := []OpCode{} for _, c := range codes { i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 // End the current group and start a new one whenever // there is a large range with no changes. if c.Tag == OpEqual && i2-i1 > nn { group = append(group, OpCode{ c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n), }) groups = append(groups, group) group = []OpCode{} i1, j1 = max(i1, i2-n), max(j1, j2-n) } group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) } if len(group) > 0 && !(len(group) == 1 && group[0].Tag == OpEqual) { groups = append(groups, group) } return groups } go-snaps-0.5.16/internal/difflib/difflib_test.go000066400000000000000000000071321511036776100215440ustar00rootroot00000000000000package difflib import ( "fmt" "strings" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestGetOptCodes(t *testing.T) { for _, v := range []struct { name string a string b string expected []OpCode }{ { name: "qabxcd, abycdf", a: "qabxcd", b: "abycdf", expected: []OpCode{ {Tag: OpDelete, I1: 0, I2: 1, J1: 0, J2: 0}, // d a[0:1], (q) b[0:0] () {Tag: OpEqual, I1: 1, I2: 3, J1: 0, J2: 2}, // e a[1:3], (ab) b[0:2] (ab) {Tag: OpReplace, I1: 3, I2: 4, J1: 2, J2: 3}, // r a[3:4], (x) b[2:3] (y) {Tag: OpEqual, I1: 4, I2: 6, J1: 3, J2: 5}, // e a[4:6], (cd) b[3:5] (cd) {Tag: OpInsert, I1: 6, I2: 6, J1: 5, J2: 6}, // i a[6:6], () b[5:6] (f) }, }, { name: "AsciiOnDelete", a: strings.Repeat("a", 40) + "c" + strings.Repeat("b", 40), b: strings.Repeat("a", 40) + strings.Repeat("b", 40), expected: []OpCode{ {OpEqual, 0, 40, 0, 40}, {OpDelete, 40, 41, 40, 40}, {OpEqual, 41, 81, 40, 80}, }, }, { name: "AsciiOneInsert - 1", a: strings.Repeat("b", 100), b: "a" + strings.Repeat("b", 100), expected: []OpCode{ {OpInsert, 0, 0, 0, 1}, {OpEqual, 0, 100, 1, 101}, }, }, { name: "AsciiOneInsert - 2", a: strings.Repeat("b", 100), b: strings.Repeat("b", 50) + "a" + strings.Repeat("b", 50), expected: []OpCode{ {OpEqual, 0, 50, 0, 50}, {OpInsert, 50, 50, 50, 51}, {OpEqual, 50, 100, 51, 101}, }, }, } { v := v t.Run(v.name, func(t *testing.T) { t.Parallel() a := strings.Split(v.a, "") b := strings.Split(v.b, "") test.Equal(t, v.expected, NewMatcher(a, b).getOpCodes()) }) } } func TestGroupedOpCodes(t *testing.T) { a := []string{} for i := 0; i != 39; i++ { a = append(a, fmt.Sprintf("%02d", i)) } b := []string{} b = append(b, a[:8]...) b = append(b, " i") b = append(b, a[8:19]...) b = append(b, " x") b = append(b, a[20:22]...) b = append(b, a[27:34]...) b = append(b, " y") b = append(b, a[35:]...) s := NewMatcher(a, b) w := &strings.Builder{} for _, g := range s.GetGroupedOpCodes(-1) { fmt.Fprintf(w, "group\n") for _, op := range g { fmt.Fprintf(w, " %d, %d, %d, %d, %d\n", op.Tag, op.I1, op.I2, op.J1, op.J2) } } expected := `group 0, 5, 8, 5, 8 1, 8, 8, 8, 9 0, 8, 11, 9, 12 group 0, 16, 19, 17, 20 3, 19, 20, 20, 21 0, 20, 22, 21, 23 2, 22, 27, 23, 23 0, 27, 30, 23, 26 group 0, 31, 34, 27, 30 3, 34, 35, 30, 31 0, 35, 38, 31, 34 ` test.Equal(t, expected, w.String()) } func TestOutputFormatRangeFormatUnified(t *testing.T) { // Per the diff spec at http://www.unix.org/single_unix_specification/ // // Each field shall be of the form: // %1d", if the range contains exactly one line, // and: // "%1d,%1d", , otherwise. // If a range is empty, its beginning line number shall be the number of // the line just before the range, or 0 if the empty range starts the file. test.Equal(t, "3,0", FormatRangeUnified(3, 3)) test.Equal(t, "4", FormatRangeUnified(3, 4)) test.Equal(t, "4,2", FormatRangeUnified(3, 5)) test.Equal(t, "4,3", FormatRangeUnified(3, 6)) test.Equal(t, "0,0", FormatRangeUnified(0, 0)) } func TestFindLongest(t *testing.T) { a := strings.Split("dabcd", "") b := strings.Split(strings.Repeat("d", 100)+"abc"+strings.Repeat("d", 100), "") m := NewMatcher(a, b) match := m.findLongestMatch(0, len(a), 0, len(b)) test.Equal(t, 0, match.A) test.Equal(t, 99, match.B) test.Equal(t, 5, match.Size) test.Equal(t, a[match.A:match.A+match.Size], b[match.B:match.B+match.Size]) } go-snaps-0.5.16/internal/test/000077500000000000000000000000001511036776100161345ustar00rootroot00000000000000go-snaps-0.5.16/internal/test/test.go000066400000000000000000000071451511036776100174510ustar00rootroot00000000000000package test import ( "os" "path/filepath" "reflect" "strings" "testing" "github.com/kr/pretty" ) type MockTestingT struct { MockHelper func() MockName func() string MockSkip func(...any) MockSkipf func(string, ...any) MockSkipNow func() MockError func(...any) MockLog func(...any) MockCleanup func(func()) t *testing.T } // NewMockTestingT returns a MockTestingT with common default values func NewMockTestingT(t *testing.T) MockTestingT { return MockTestingT{ MockHelper: func() {}, MockCleanup: func(f func()) { f() }, MockName: func() string { return "mock-name" }, t: t, } } func (m MockTestingT) Error(args ...any) { m.t.Helper() if m.MockError == nil { m.t.Errorf( "t.Error was not expected to be called. Called with args %s", pretty.Sprint(args), ) return } m.MockError(args...) } func (m MockTestingT) Helper() { m.t.Helper() m.MockHelper() } func (m MockTestingT) Skip(args ...any) { m.t.Helper() if m.MockSkip == nil { m.t.Errorf("t.Skip was not expected to be called. Called with args %s", pretty.Sprint(args)) return } m.MockSkip(args...) } func (m MockTestingT) Skipf(format string, args ...any) { m.t.Helper() if m.MockSkipf == nil { m.t.Errorf( "t.Skipf was not expected to be called. Called with format: %s and args %s", format, pretty.Sprint(args), ) return } m.MockSkipf(format, args...) } func (m MockTestingT) SkipNow() { m.t.Helper() if m.MockSkipNow == nil { m.t.Errorf("t.SkipNow was not expected to be called") return } m.MockSkipNow() } func (m MockTestingT) Name() string { m.t.Helper() if m.MockName == nil { m.t.Errorf("t.Name was not expected to be called") return "" } return m.MockName() } func (m MockTestingT) Log(args ...any) { m.t.Helper() if m.MockLog == nil { m.t.Errorf("t.Log was not expected to be called. Called with args %s", pretty.Sprint(args)) return } m.MockLog(args...) } func (m MockTestingT) Cleanup(f func()) { m.t.Helper() if m.MockCleanup == nil { m.t.Errorf("t.Cleanup was not expected to be called") return } m.MockCleanup(f) } // Equal asserts expected and received have deep equality func Equal[A any](t *testing.T, expected, received A) { t.Helper() if !reflect.DeepEqual(expected, received) { t.Errorf("\n[expected]: %v\n[received]: %v", expected, received) } } // Contains reports whether a substr is inside s func Contains(t *testing.T, s, substr string) { t.Helper() if !strings.Contains(s, substr) { t.Errorf("\n [expected] %s to contain %s", s, substr) } } func HasSuffix(t *testing.T, s, suffix string) { t.Helper() if !strings.HasSuffix(s, suffix) { t.Errorf("\n [expected] %s to have suffix %s", s, suffix) } } func CreateTempFile(t *testing.T, data string) string { dir := t.TempDir() path := filepath.Join(dir, "mock.file") _ = os.WriteFile(path, []byte(data), os.ModePerm) return path } // GetFileContent returns the contents of a file // // it errors if file doesn't exist func GetFileContent(t *testing.T, name string) string { t.Helper() content, err := os.ReadFile(name) if err != nil { t.Error(err) } return string(content) } func True(t *testing.T, val bool) { t.Helper() if !val { t.Error("expected true but got false") } } func False(t *testing.T, val bool) { t.Helper() if val { t.Error("expected false but got true") } } func Nil(t *testing.T, val any) { t.Helper() v := reflect.ValueOf(val) if val != nil && !v.IsNil() { t.Errorf("expected nil but got %v", val) } } func NoError(t *testing.T, err error) { t.Helper() if err != nil { t.Errorf("expected no error but got %s", err) } } go-snaps-0.5.16/match/000077500000000000000000000000001511036776100144355ustar00rootroot00000000000000go-snaps-0.5.16/match/any.go000066400000000000000000000050111511036776100155500ustar00rootroot00000000000000package match import ( "bytes" "github.com/gkampitakis/go-snaps/match/internal/yaml" "github.com/goccy/go-yaml/parser" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) type anyMatcher struct { paths []string placeholder any errOnMissingPath bool name string } func (a *anyMatcher) matcherError(err error, path string) MatcherError { return MatcherError{ Reason: err, Matcher: a.name, Path: path, } } /* Any matcher acts as a placeholder for any value It replaces any targeted path with a placeholder string Any("user.name", "user.email") // or for yaml Any("$.user.name", "$.user.email") */ func Any(paths ...string) *anyMatcher { return &anyMatcher{ errOnMissingPath: true, placeholder: "", paths: paths, name: "Any", } } // Placeholder allows to define the placeholder value for Any matcher func (a *anyMatcher) Placeholder(p any) *anyMatcher { a.placeholder = p return a } // ErrOnMissingPath determines if matcher will fail in case of trying to access a path // that doesn't exist func (a *anyMatcher) ErrOnMissingPath(e bool) *anyMatcher { a.errOnMissingPath = e return a } // YAML is intended to be called internally on snaps.MatchYAML for applying Any matchers func (a anyMatcher) YAML(b []byte) ([]byte, []MatcherError) { var errs []MatcherError f, err := parser.ParseBytes(b, parser.ParseComments) if err != nil { return b, []MatcherError{a.matcherError(err, "*")} } for _, p := range a.paths { path, _, exists, err := yaml.Get(f, p) if err != nil { errs = append(errs, a.matcherError(err, p)) continue } if !exists { if a.errOnMissingPath { errs = append(errs, a.matcherError(errPathNotFound, p)) } continue } if err := yaml.Update(f, path, a.placeholder); err != nil { errs = append(errs, a.matcherError(err, p)) continue } } return yaml.MarshalFile(f, bytes.HasSuffix(b, []byte("\n"))), errs } // JSON is intended to be called internally on snaps.MatchJSON for applying Any matchers func (a anyMatcher) JSON(b []byte) ([]byte, []MatcherError) { var errs []MatcherError json := b for _, path := range a.paths { r := gjson.GetBytes(json, path) if !r.Exists() { if a.errOnMissingPath { errs = append(errs, a.matcherError(errPathNotFound, path)) } continue } j, err := sjson.SetBytesOptions(json, path, a.placeholder, setJSONOptions) if err != nil { errs = append(errs, a.matcherError(err, path)) continue } json = j } return json, errs } go-snaps-0.5.16/match/any_test.go000066400000000000000000000070141511036776100166140ustar00rootroot00000000000000package match import ( "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestAnyMatcher(t *testing.T) { t.Run("should create an any matcher", func(t *testing.T) { p := []string{"test.1", "test.2"} a := Any(p...) test.True(t, a.errOnMissingPath) test.Equal(t, "", a.placeholder) test.Equal(t, p, a.paths) test.Equal(t, "Any", a.name) }) t.Run("should allow overriding values", func(t *testing.T) { p := []string{"test.1", "test.2"} a := Any(p...).ErrOnMissingPath(false).Placeholder("hello") test.False(t, a.errOnMissingPath) test.Equal(t, "hello", a.placeholder) test.Equal(t, p, a.paths) test.Equal(t, "Any", a.name) }) t.Run("JSON", func(t *testing.T) { j := []byte(`{ "user": { "name": "mock-user", "email": "mock-email" }, "date": "16/10/2022" }`) t.Run("should return error in case of missing path", func(t *testing.T) { a := Any("user.2") res, errs := a.JSON(j) test.Equal(t, j, res) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "path does not exist", err.Reason.Error()) test.Equal(t, "Any", err.Matcher) test.Equal(t, "user.2", err.Path) }) t.Run("should aggregate errors", func(t *testing.T) { a := Any("user.2", "user.3") res, errs := a.JSON(j) test.Equal(t, j, res) test.Equal(t, 2, len(errs)) }) t.Run("should replace value and return new json", func(t *testing.T) { a := Any("user.email", "date", "missing.key").ErrOnMissingPath(false) res, errs := a.JSON(j) expected := `{ "user": { "name": "mock-user", "email": "" }, "date": "" }` test.Equal(t, 0, len(errs)) test.Equal(t, expected, string(res)) }) t.Run( "should replace value and return new json with different placeholder", func(t *testing.T) { a := Any( "user.email", "date", "missing.key", ).ErrOnMissingPath( false, ).Placeholder(10) res, errs := a.JSON(j) expected := `{ "user": { "name": "mock-user", "email": 10 }, "date": 10 }` test.Equal(t, 0, len(errs)) test.Equal(t, expected, string(res)) }, ) }) t.Run("YAML", func(t *testing.T) { y := []byte(`user: name: mock-user email: mock-email date: 16/10/2022 `) t.Run("should return error in case of missing path", func(t *testing.T) { a := Any("$.user.missing") res, errs := a.YAML(y) test.Equal(t, y, res) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "path does not exist", err.Reason.Error()) test.Equal(t, "Any", err.Matcher) test.Equal(t, "$.user.missing", err.Path) }) t.Run("should aggregate errors", func(t *testing.T) { a := Any("$.user.missing.key", "$.user.missing.key1") res, errs := a.YAML(y) test.Equal(t, y, res) test.Equal(t, 2, len(errs)) }) t.Run("should replace value and return new yaml", func(t *testing.T) { a := Any("$.user.email", "$.date", "$.missing.key").ErrOnMissingPath(false) res, errs := a.YAML(y) expected := `user: name: mock-user email: date: ` test.Equal(t, 0, len(errs)) test.Equal(t, expected, string(res)) }) t.Run( "should replace value and return new yaml with different placeholder", func(t *testing.T) { a := Any( "$.user.email", "$.date", "$.missing.key", ).ErrOnMissingPath(false). Placeholder(10) res, errs := a.YAML(y) expected := `user: name: mock-user email: 10 date: 10 ` test.Equal(t, 0, len(errs)) test.Equal(t, expected, string(res)) }, ) }) } go-snaps-0.5.16/match/custom.go000066400000000000000000000060611511036776100163010ustar00rootroot00000000000000package match import ( "bytes" "github.com/gkampitakis/go-snaps/match/internal/yaml" "github.com/goccy/go-yaml/parser" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) type customMatcher struct { callback func(val any) (any, error) errOnMissingPath bool name string path string } func (c *customMatcher) matcherError(err error) []MatcherError { return []MatcherError{{ Reason: err, Matcher: c.name, Path: c.path, }} } type CustomCallback func(val any) (any, error) /* Custom matcher allows you to bring your own validation and placeholder value. match.Custom("user.age", func(val any) (any, error) { age, ok := val.(float64) if !ok { return nil, fmt.Errorf("expected number but got %T", val) } return "some number", nil }) The callback func value for JSON can be one of these types: bool // for JSON booleans float64 // for JSON numbers string // for JSON string literals nil // for JSON null map[string]any // for JSON objects []any // for JSON arrays The callback func value for YAML can be one of these types: bool // for YAML booleans float64 // for YAML float numbers uint64 // for YAML integer numbers string // for YAML string literals nil // for YAML null map[string]any // for YAML objects []any // for YAML arrays */ func Custom(path string, callback CustomCallback) *customMatcher { return &customMatcher{ errOnMissingPath: true, callback: callback, name: "Custom", path: path, } } // ErrOnMissingPath determines if Matcher will fail in case of trying to access a json path // that doesn't exist func (c *customMatcher) ErrOnMissingPath(e bool) *customMatcher { c.errOnMissingPath = e return c } // YAML is intended to be called internally on snaps.MatchYAML for applying Custom matcher func (c *customMatcher) YAML(b []byte) ([]byte, []MatcherError) { f, err := parser.ParseBytes(b, parser.ParseComments) if err != nil { return nil, c.matcherError(err) } path, node, exists, err := yaml.Get(f, c.path) if err != nil { return nil, c.matcherError(err) } if !exists { if c.errOnMissingPath { return nil, c.matcherError(errPathNotFound) } return b, nil } value, err := yaml.GetValue(node) if err != nil { return nil, c.matcherError(err) } result, err := c.callback(value) if err != nil { return nil, c.matcherError(err) } if err := yaml.Update(f, path, result); err != nil { return nil, c.matcherError(err) } return yaml.MarshalFile(f, bytes.HasSuffix(b, []byte("\n"))), nil } // JSON is intended to be called internally on snaps.MatchJSON for applying Custom matcher func (c *customMatcher) JSON(b []byte) ([]byte, []MatcherError) { r := gjson.GetBytes(b, c.path) if !r.Exists() { if c.errOnMissingPath { return nil, c.matcherError(errPathNotFound) } return b, nil } value, err := c.callback(r.Value()) if err != nil { return nil, c.matcherError(err) } b, err = sjson.SetBytesOptions(b, c.path, value, setJSONOptions) if err != nil { return nil, c.matcherError(err) } return b, nil } go-snaps-0.5.16/match/custom_test.go000066400000000000000000000074421511036776100173440ustar00rootroot00000000000000package match import ( "errors" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestCustomMatcher(t *testing.T) { t.Run("should create a custom matcher", func(t *testing.T) { c := Custom("path", func(val any) (any, error) { return nil, nil }) test.True(t, c.errOnMissingPath) test.Equal(t, c.path, "path") test.Equal(t, c.name, "Custom") }) t.Run("should allow overriding values", func(t *testing.T) { c := Custom("path", func(val any) (any, error) { return nil, nil }).ErrOnMissingPath(false) test.False(t, c.errOnMissingPath) test.Equal(t, c.path, "path") test.Equal(t, c.name, "Custom") }) t.Run("JSON", func(t *testing.T) { j := []byte(`{ "user": { "name": "mock-user", "email": "mock-email" }, "date": "16/10/2022" }`) t.Run("should return error in case of missing path", func(t *testing.T) { c := Custom("missing.key", func(val any) (any, error) { return nil, nil }) res, errs := c.JSON(j) test.Nil(t, res) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "path does not exist", err.Reason.Error()) test.Equal(t, "Custom", err.Matcher) test.Equal(t, "missing.key", err.Path) }) t.Run("should ignore error in case of missing path", func(t *testing.T) { c := Custom("missing.key", func(val any) (any, error) { return nil, nil }).ErrOnMissingPath(false) res, errs := c.JSON(j) test.Equal(t, j, res) test.Nil(t, errs) }) t.Run("should return error from custom callback", func(t *testing.T) { c := Custom("user.email", func(val any) (any, error) { return nil, errors.New("custom error") }) res, errs := c.JSON(j) test.Nil(t, res) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "custom error", err.Reason.Error()) test.Equal(t, "Custom", err.Matcher) test.Equal(t, "user.email", err.Path) }) t.Run("should apply value from custom callback to json", func(t *testing.T) { c := Custom("user.email", func(val any) (any, error) { return "replaced email", nil }) res, errs := c.JSON(j) expected := `{ "user": { "name": "mock-user", "email": "replaced email" }, "date": "16/10/2022" }` test.Equal(t, expected, string(res)) test.Nil(t, errs) }) }) t.Run("YAML", func(t *testing.T) { y := []byte(` user: name: mock-user email: mock-email date: 16/10/2022 `) t.Run("should return error in case of missing path", func(t *testing.T) { c := Custom("$.missing.key", func(val any) (any, error) { return nil, nil }) res, errs := c.YAML(y) test.Nil(t, res) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "path does not exist", err.Reason.Error()) test.Equal(t, "Custom", err.Matcher) test.Equal(t, "$.missing.key", err.Path) }) t.Run("should ignore error in case of missing path", func(t *testing.T) { c := Custom("$.missing.key", func(val any) (any, error) { return nil, nil }).ErrOnMissingPath(false) res, errs := c.YAML(y) test.Equal(t, y, res) test.Nil(t, errs) }) t.Run("should return error from custom callback", func(t *testing.T) { c := Custom("$.user.email", func(val any) (any, error) { return nil, errors.New("custom error") }) res, errs := c.YAML(y) test.Nil(t, res) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "custom error", err.Reason.Error()) test.Equal(t, "Custom", err.Matcher) test.Equal(t, "$.user.email", err.Path) }) t.Run("should apply value from custom callback to yaml", func(t *testing.T) { c := Custom("$.user.email", func(val any) (any, error) { return "replaced email", nil }) res, errs := c.YAML(y) expected := `user: name: mock-user email: replaced email date: 16/10/2022 ` test.Equal(t, expected, string(res)) test.Nil(t, errs) }) }) } go-snaps-0.5.16/match/internal/000077500000000000000000000000001511036776100162515ustar00rootroot00000000000000go-snaps-0.5.16/match/internal/yaml/000077500000000000000000000000001511036776100172135ustar00rootroot00000000000000go-snaps-0.5.16/match/internal/yaml/yaml.go000066400000000000000000000027201511036776100205050ustar00rootroot00000000000000package yaml import ( "bytes" "errors" "strings" "github.com/goccy/go-yaml" "github.com/goccy/go-yaml/ast" ) // GetValue returns the value of the node. func GetValue(node ast.Node) (any, error) { data, err := node.MarshalYAML() if err != nil { return nil, err } var value any if err := yaml.Unmarshal(data, &value); err != nil { return nil, err } return value, nil } // Get takes an ast.File and a string representing a path // and returns the yaml.Path, the node and a bool indicating if the node exists. func Get(f *ast.File, p string) (*yaml.Path, ast.Node, bool, error) { path, err := yaml.PathString(p) if err != nil { return nil, nil, false, err } node, err := path.FilterFile(f) if err != nil { if errors.Is(err, yaml.ErrNotFoundNode) { return path, nil, false, nil } return path, nil, false, err } return path, node, true, nil } // Update marshals the value and replaces the file at the path provided with the new value. func Update(f *ast.File, path *yaml.Path, value any) error { b, err := yaml.Marshal(value) if err != nil { return err } return path.ReplaceWithReader(f, bytes.NewReader(b)) } // MarshalFile returns the representation of the ast.File to a byte slice. func MarshalFile(f *ast.File, addNewLine bool) []byte { docs := make([]string, 0, len(f.Docs)) for _, doc := range f.Docs { docs = append(docs, doc.String()) } if addNewLine { docs = append(docs, "") } return []byte(strings.Join(docs, "\n")) } go-snaps-0.5.16/match/type.go000066400000000000000000000063461511036776100157560ustar00rootroot00000000000000package match import ( "bytes" "fmt" "github.com/gkampitakis/go-snaps/match/internal/yaml" "github.com/goccy/go-yaml/parser" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) type typeMatcher[ExpectedType any] struct { paths []string errOnMissingPath bool name string expectedType any } func (t *typeMatcher[ExpectedType]) matcherError(err error, path string) MatcherError { return MatcherError{ Reason: err, Matcher: t.name, Path: path, } } /* Type matcher evaluates types that are passed in a snapshot It replaces any targeted path with placeholder in the form of `` match.Type[string]("user.info", "user.age") // or for yaml match.Type[string]("$.user.info", "$.user.age") */ func Type[ExpectedType any](paths ...string) *typeMatcher[ExpectedType] { return &typeMatcher[ExpectedType]{ paths: paths, errOnMissingPath: true, name: "Type", expectedType: *new(ExpectedType), } } // ErrOnMissingPath determines if matcher will fail in case of trying to access a path // that doesn't exist func (t *typeMatcher[T]) ErrOnMissingPath(e bool) *typeMatcher[T] { t.errOnMissingPath = e return t } // YAML is intended to be called internally on snaps.MatchJSON for applying Type matchers func (t typeMatcher[ExpectedType]) YAML(b []byte) ([]byte, []MatcherError) { var errs []MatcherError f, err := parser.ParseBytes(b, parser.ParseComments) if err != nil { return b, []MatcherError{t.matcherError(err, "*")} } for _, p := range t.paths { path, node, exists, err := yaml.Get(f, p) if err != nil { errs = append(errs, t.matcherError(err, p)) continue } if !exists { if t.errOnMissingPath { errs = append(errs, t.matcherError(errPathNotFound, p)) } continue } value, err := yaml.GetValue(node) if err != nil { errs = append(errs, t.matcherError(err, p)) continue } if err := typeCheck[ExpectedType](value); err != nil { errs = append(errs, t.matcherError(err, p)) continue } if err := yaml.Update(f, path, typePlaceholder(value)); err != nil { errs = append(errs, t.matcherError(err, p)) continue } } return yaml.MarshalFile(f, bytes.HasSuffix(b, []byte("\n"))), errs } // JSON is intended to be called internally on snaps.MatchJSON for applying Type matchers func (t typeMatcher[ExpectedType]) JSON(b []byte) ([]byte, []MatcherError) { var errs []MatcherError json := b for _, path := range t.paths { r := gjson.GetBytes(json, path) if !r.Exists() { if t.errOnMissingPath { errs = append(errs, t.matcherError(errPathNotFound, path)) } continue } if err := typeCheck[ExpectedType](r.Value()); err != nil { errs = append(errs, t.matcherError(err, path)) continue } j, err := sjson.SetBytesOptions( json, path, typePlaceholder(r.Value()), setJSONOptions, ) if err != nil { errs = append(errs, t.matcherError(err, path)) continue } json = j } return json, errs } func typeCheck[ExpectedType any](value any) error { if _, ok := value.(ExpectedType); !ok { return fmt.Errorf("expected type %T, received %T", *new(ExpectedType), value) } return nil } func typePlaceholder(value any) string { return fmt.Sprintf("", value) } go-snaps-0.5.16/match/type_test.go000066400000000000000000000062531511036776100170120ustar00rootroot00000000000000package match import ( "reflect" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestTypeMatcher(t *testing.T) { t.Run("should create a type matcher", func(t *testing.T) { p := []string{"test.1", "test.2"} tm := Type[string](p...) test.True(t, tm.errOnMissingPath) test.Equal(t, "Type", tm.name) test.Equal(t, p, tm.paths) test.Equal(t, reflect.TypeOf("").String(), reflect.TypeOf(tm.expectedType).String()) }) t.Run("should allow overriding values", func(t *testing.T) { p := []string{"test.1", "test.2"} tm := Type[string](p...) tm.ErrOnMissingPath(false) test.False(t, tm.errOnMissingPath) test.Equal(t, "Type", tm.name) test.Equal(t, p, tm.paths) test.Equal(t, reflect.TypeOf("").String(), reflect.TypeOf(tm.expectedType).String()) }) t.Run("JSON", func(t *testing.T) { j := []byte(`{ "user": { "name": "mock-user", "email": "mock-email", "age": 29 }, "date": "16/10/2022" }`) t.Run("should return error in case of missing path", func(t *testing.T) { tm := Type[string]("user.2") res, errs := tm.JSON(j) test.Equal(t, j, res) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "path does not exist", err.Reason.Error()) test.Equal(t, "Type", err.Matcher) test.Equal(t, "user.2", err.Path) }) t.Run("should aggregate errors", func(t *testing.T) { tm := Type[string]("user.2", "user.3") res, errs := tm.JSON(j) test.Equal(t, j, res) test.Equal(t, 2, len(errs)) }) t.Run("should evaluate passed type and replace json", func(t *testing.T) { tm := Type[string]("user.name", "date") res, errs := tm.JSON(j) expected := `{ "user": { "name": "", "email": "mock-email", "age": 29 }, "date": "" }` test.Nil(t, errs) test.Equal(t, expected, string(res)) }) t.Run("should return error with type mismatch", func(t *testing.T) { tm := Type[int]("user.name", "user.age") _, errs := tm.JSON(j) test.Equal(t, 2, len(errs)) test.Equal(t, "expected type int, received string", errs[0].Reason.Error()) test.Equal(t, "expected type int, received float64", errs[1].Reason.Error()) }) }) t.Run("YAML", func(t *testing.T) { y := []byte(`user: name: mock-user email: mock-email age: 29 date: 16/10/2022 `) t.Run("should return error in case of missing path", func(t *testing.T) { tm := Type[string]("$.user.missing") res, errs := tm.YAML(y) test.Equal(t, string(y), string(res)) test.Equal(t, 1, len(errs)) err := errs[0] test.Equal(t, "path does not exist", err.Reason.Error()) test.Equal(t, "Type", err.Matcher) test.Equal(t, "$.user.missing", err.Path) }) t.Run("should aggregate errors", func(t *testing.T) { tm := Type[string]("$.user.missing", "$.user.missing_key") res, errs := tm.YAML(y) test.Equal(t, y, res) test.Equal(t, 2, len(errs)) }) t.Run("should evaluate passed type and replace yaml", func(t *testing.T) { tm := Type[string]("$.user.name", "$.date") res, errs := tm.YAML(y) expected := `user: name: email: mock-email age: 29 date: ` test.Nil(t, errs) test.Equal(t, expected, string(res)) }) }) } go-snaps-0.5.16/match/utils.go000066400000000000000000000007401511036776100161250ustar00rootroot00000000000000package match import ( "errors" "github.com/tidwall/sjson" ) var ( errPathNotFound = errors.New("path does not exist") setJSONOptions = &sjson.Options{ Optimistic: true, ReplaceInPlace: true, } ) type JSONMatcher interface { JSON([]byte) ([]byte, []MatcherError) } type YAMLMatcher interface { YAML([]byte) ([]byte, []MatcherError) } // internal Error struct returned from Matchers type MatcherError struct { Reason error Matcher string Path string } go-snaps-0.5.16/snaps/000077500000000000000000000000001511036776100144655ustar00rootroot00000000000000go-snaps-0.5.16/snaps/__snapshots__/000077500000000000000000000000001511036776100173035ustar00rootroot00000000000000go-snaps-0.5.16/snaps/__snapshots__/clean_test.snap000077500000000000000000000033261511036776100223160ustar00rootroot00000000000000 [TestSummary/should_print_obsolete_file - 1] Snapshot Summary  › 1 snapshot file obsolete  ↳ • test0.snap  To remove it, re-run tests with `UPDATE_SNAPS=clean go test ./...`  --- [TestSummary/should_print_obsolete_tests - 1] Snapshot Summary  › 2 snapshot tests obsolete  ↳ • TestMock/should_pass - 1  ↳ • TestMock/should_pass - 2  To remove them, re-run tests with `UPDATE_SNAPS=clean go test ./...`  --- [TestSummary/should_print_updated_file - 1] Snapshot Summary  › 1 snapshot file removed  ↳ • test0.snap  --- [TestSummary/should_print_updated_test - 1] Snapshot Summary  › 1 snapshot test removed  ↳ • TestMock/should_pass - 1  --- [TestSummary/should_return_empty_string - 1] --- [TestSummary/should_print_events - 1] Snapshot Summary āœ“ 10 snapshots passed āœ• 100 snapshots failed āœŽ 5 snapshots added āœŽ 3 snapshots updated  --- [TestSummary/should_print_number_of_skipped_tests - 1] Snapshot Summary ⟳ 1 snapshot skipped  --- [TestSummary/should_print_all_summary - 1] Snapshot Summary āœ“ 10 snapshots passed āœ• 100 snapshots failed āœŽ 5 snapshots added āœŽ 3 snapshots updated ⟳ 5 snapshots skipped  › 1 snapshot file obsolete  ↳ • test0.snap  › 1 snapshot test obsolete  ↳ • TestMock/should_pass - 1  To remove them, re-run tests with `UPDATE_SNAPS=clean go test ./...`  --- go-snaps-0.5.16/snaps/__snapshots__/diff_test.snap000077500000000000000000000222121511036776100221370ustar00rootroot00000000000000 [TestDiff/should_build_diff_report_consistently - 1] - Snapshot - 20 + Received + 10000 mock-diff at snap/path:10  --- [TestDiff/should_build_diff_report_consistently - 2] - Snapshot - 10000 + Received + 20 mock-diff at snap/path:20  --- [TestDiff/with_color/should_apply_highlights_on_single_line_diff - 1] - Snapshot - 20 + Received + 20 - abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd + abcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcf at snap/path:10  --- [TestDiff/with_color/multiline_diff - 1] - Snapshot - 5 + Received + 6 @@ -1,12 +1,13 @@ Proin justo libero, pellentesque sit amet scelerisque ut, sollicitudin non tortor. -  Sed velit elit, accumsan sed porttitor nec, elementum quis sapien.  +  Sed velit elit, accumsan sed Ipsum nec, elementum quis sapien.   Phasellus mattis purus in dui pretium, eu euismod metus feugiat. -  Morbi turpis tellus, tincidunt mollis rutrum at, mattis laoreet lacus.  +  Morbi turpis Lorem, tincidunt mollis rutrum at, mattis laoreet lacus.   Donec in quam tempus, eleifend erat sit amet, aliquet metus.   Sed ullamcorper velit a est efficitur, et tempus ante rhoncus.   Aliquam diam sapien, vulputate sit amet elit sit amet, commodo eleifend sapien.   Donec consequat at nibh id mattis. Quisque vitae sagittis eros, convallis consectetur ante.   Duis finibus suscipit mi sed consectetur. Nulla libero neque, sagittis vel nulla vel, - vestibulum sagittis mauris. Ut laoreet urna lectus.  - Sed lorem felis, condimentum eget vehicula non, sagittis sit amet diam.  - Vivamus ut sapien at erat imperdiet suscipit id a lectus. + vestibulum sagittis mauris. Ut laoreet urna lectus.  + Sed lorem felis, condimentum eget vehicula non, sagittis sit amet diam.  + Vivamus ut sapien at erat imperdiet suscipit id a lectus. + Another Line added. at snap/path:10  --- [TestDiff/no_color/should_apply_highlights_on_single_line_diff - 1] - Snapshot - 1 + Received + 1 - abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd + abcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcfabcf at snap/path:10 --- [TestDiff/no_color/multiline_diff - 1] - Snapshot - 5 + Received + 6 @@ -1,12 +1,13 @@ Proin justo libero, pellentesque sit amet scelerisque ut, sollicitudin non tortor. - Sed velit elit, accumsan sed porttitor nec, elementum quis sapien. + Sed velit elit, accumsan sed Ipsum nec, elementum quis sapien. Phasellus mattis purus in dui pretium, eu euismod metus feugiat. - Morbi turpis tellus, tincidunt mollis rutrum at, mattis laoreet lacus. + Morbi turpis Lorem, tincidunt mollis rutrum at, mattis laoreet lacus. Donec in quam tempus, eleifend erat sit amet, aliquet metus. Sed ullamcorper velit a est efficitur, et tempus ante rhoncus. Aliquam diam sapien, vulputate sit amet elit sit amet, commodo eleifend sapien. Donec consequat at nibh id mattis. Quisque vitae sagittis eros, convallis consectetur ante. Duis finibus suscipit mi sed consectetur. Nulla libero neque, sagittis vel nulla vel, - vestibulum sagittis mauris. Ut laoreet urna lectus. - Sed lorem felis, condimentum eget vehicula non, sagittis sit amet diam. - Vivamus ut sapien at erat imperdiet suscipit id a lectus. + vestibulum sagittis mauris. Ut laoreet urna lectus. + Sed lorem felis, condimentum eget vehicula non, sagittis sit amet diam. + Vivamus ut sapien at erat imperdiet suscipit id a lectus. + Another Line added. at snap/path:20 --- [TestDiff/should_not_print_snapshot_line_if_not_provided - 1] - Snapshot - 2 + Received + 10 there is a diff here --- [TestDiff/should_print_newline_diffs/multiline - 1] - Snapshot - 0 + Received + 3 snapshot +   with multiple lines +  + diff   at snap/path:10  --- [TestDiff/should_print_newline_diffs/multiline - 2] - Snapshot - 3 + Received + 0 snapshot -   with multiple lines -  - diff   at snap/path:10  --- [TestDiff/should_print_newline_diffs/singleline - 1] - Snapshot - 0 + Received + 1 - single line snap + single line snap ↵ at snap/path:10  --- [TestDiff/should_print_newline_diffs/singleline - 2] - Snapshot - 0 + Received + 1 - single line snap + single line snap ↵ at snap/path:10  --- [TestDiff/should_print_newline_diffs/singleline - 3] - Snapshot - 0 + Received + 1 - single line snap + single line snap↵ at snap/path:10  --- [TestDiff/should_print_newline_diffs/singleline - 4] - Snapshot - 1 + Received + 0 - single line snap↵ + single line snap at snap/path:10  --- go-snaps-0.5.16/snaps/basecaller_test.go000066400000000000000000000020351511036776100201500ustar00rootroot00000000000000package snaps import ( "testing" "github.com/gkampitakis/go-snaps/internal/test" ) // This file is "sensitive" to line changes. func TestBaseCallerNested(t *testing.T) { file, line := baseCaller(0) test.Contains(t, file, "snaps/basecaller_test.go") test.Equal(t, 12, line) } func testBaseCallerNested(t *testing.T) { file, line := baseCaller(0) test.Contains(t, file, "snaps/basecaller_test.go") test.Equal(t, 19, line) } func TestBaseCallerHelper(t *testing.T) { t.Helper() file, line := baseCaller(0) test.Contains(t, file, "snaps/basecaller_test.go") test.Equal(t, 27, line) } func TestBaseCaller(t *testing.T) { t.Run("should return correct baseCaller", func(t *testing.T) { var file string var line int func() { file, line = baseCaller(1) }() test.Contains(t, file, "snaps/basecaller_test.go") test.Equal(t, 40, line) }) t.Run("should return parent function", func(t *testing.T) { testBaseCallerNested(t) }) t.Run("should return function's name", func(t *testing.T) { TestBaseCallerNested(t) }) } go-snaps-0.5.16/snaps/clean.go000066400000000000000000000232301511036776100160760ustar00rootroot00000000000000package snaps import ( "bytes" "flag" "fmt" "io" "os" "path/filepath" "slices" "strconv" "strings" "sync" "testing" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/maruel/natural" ) var testEvents = newTestEvents() const ( erred uint8 = iota added updated passed ) type events struct { items map[uint8]int sync.Mutex } func (e *events) register(event uint8) { e.Lock() defer e.Unlock() e.items[event]++ } func newTestEvents() *events { return &events{ items: make(map[uint8]int), Mutex: sync.Mutex{}, } } type CleanOpts struct { // If set to true, `go-snaps` will sort the snapshots Sort bool } // Clean runs checks for identifying obsolete snapshots and prints a Test Summary. // // Must be called in a TestMain // // func TestMain(m *testing.M) { // v := m.Run() // // // After all tests have run `go-snaps` can check for unused snapshots // dirty, err := snaps.Clean(m) // if err != nil { // fmt.Println("Error cleaning snaps:", err) // os.Exit(1) // } // if dirty { // fmt.Println("Some snapshots were outdated.") // os.Exit(1) // } // // os.Exit(v) // } // // Clean also supports options for sorting the snapshots // // func TestMain(m *testing.M) { // v := m.Run() // // // After all tests have run `go-snaps` will sort snapshots // dirty, err := snaps.Clean(m, snaps.CleanOpts{Sort: true}) // if err != nil { // fmt.Println("Error cleaning snaps:", err) // os.Exit(1) // } // if dirty { // fmt.Println("Some snapshots were outdated.") // os.Exit(1) // } // // os.Exit(v) // } func Clean(m *testing.M, opts ...CleanOpts) (bool, error) { var opt CleanOpts if len(opts) != 0 { opt = opts[0] } // This is just for making sure Clean is called from TestMain _ = m runOnly := flag.Lookup("test.run").Value.String() count, _ := strconv.Atoi(flag.Lookup("test.count").Value.String()) registeredStandaloneTests := occurrences( standaloneTestsRegistry.cleanup, count, standaloneOccurrenceFMT, ) obsoleteFiles, usedFiles, filesDirty := examineFiles( testsRegistry.cleanup, registeredStandaloneTests, runOnly, shouldClean, ) obsoleteTests, snapsDirty, err := examineSnaps( testsRegistry.cleanup, usedFiles, runOnly, count, shouldClean, opt.Sort, ) if err != nil { return snapsDirty || filesDirty, err } if s := summary( obsoleteFiles, obsoleteTests, len(skippedTests.values), testEvents.items, shouldClean, ); s != "" { fmt.Println(s) } return filesDirty || snapsDirty, nil } // getTestID will return the testID if the line is in the form of [Test... - number] func getTestID(b []byte) (string, bool) { if len(b) == 0 { return "", false } // needs to start with [Test and end with ] if !bytes.HasPrefix(b, []byte("[Test")) || b[len(b)-1] != ']' { return "", false } // needs to contain ' - ' separator := bytes.Index(b, []byte(" - ")) if separator == -1 { return "", false } // needs to have a number after the separator if !isNumber(b[separator+3 : len(b)-1]) { return "", false } return string(b[1 : len(b)-1]), true } func isNumber(b []byte) bool { for i := 0; i < len(b); i++ { if b[i] < '0' || b[i] > '9' { return false } } return true } // examineFiles traverses all the directories where snap tests where executed and checks // if "orphan" snap files exist (files containing `.snap` in their name). // // If they do they are marked as obsolete and they are either deleted if `shouldUpdate=true` or printed on the console. func examineFiles( registry map[string]map[string]int, registeredStandaloneTests set, runOnly string, shouldUpdate bool, ) (obsolete, used []string, dirtyFiles bool) { uniqueDirs := set{} for snapPaths := range registry { uniqueDirs[filepath.Dir(snapPaths)] = struct{}{} } for snapPaths := range registeredStandaloneTests { uniqueDirs[filepath.Dir(snapPaths)] = struct{}{} } for dir := range uniqueDirs { dirContents, _ := os.ReadDir(dir) for _, content := range dirContents { // this is a sanity check shouldn't have dirs inside the snapshot dirs // and only delete any `.snap` files if content.IsDir() || !strings.Contains(content.Name(), snapsExt) { continue } snapPath := filepath.Join(dir, content.Name()) if _, called := registry[snapPath]; called { used = append(used, snapPath) continue } // if it's a standalone snapshot we don't add it to used list // as we don't need it for the next step, to examine individual snaps inside the file // as it contains only one if registeredStandaloneTests.Has(snapPath) { continue } if isFileSkipped(dir, content.Name(), runOnly) { continue } obsolete = append(obsolete, snapPath) if !shouldUpdate { continue } if err := os.Remove(snapPath); err != nil { fmt.Println(err) } } } return obsolete, used, len(obsolete) > 0 } func examineSnaps( registry map[string]map[string]int, used []string, runOnly string, count int, shouldUpdate, sort bool, ) ([]string, bool, error) { obsoleteTests := []string{} tests := map[string]string{} data := bytes.Buffer{} testIDs := []string{} var isDirty bool for _, snapPath := range used { f, err := os.OpenFile(snapPath, os.O_RDWR, os.ModePerm) if err != nil { return nil, isDirty, err } var needsUpdating bool registeredTests := occurrences(registry[snapPath], count, snapshotOccurrenceFMT) s := snapshotScanner(f) for s.Scan() { b := s.Bytes() // Check if line is a test id testID, match := getTestID(b) if !match { continue } testIDs = append(testIDs, testID) if !registeredTests.Has(testID) && !testSkipped(testID, runOnly) { obsoleteTests = append(obsoleteTests, testID) needsUpdating = true removeSnapshot(s) continue } for s.Scan() { line := s.Bytes() if bytes.Equal(line, endSequenceByteSlice) { tests[testID] = data.String() data.Reset() break } data.Write(line) data.WriteByte('\n') } } if err := s.Err(); err != nil { return nil, isDirty, err } needsSorting := sort && !slices.IsSortedFunc(testIDs, naturalSort) // if we're not allowed to update anything, just capture if the snapshot // needs cleaning, and then continue to the next snapshot if !shouldUpdate { if needsUpdating || needsSorting { isDirty = true } f.Close() clear(tests) testIDs = testIDs[:0] data.Reset() continue } if needsSorting { // sort testIDs slices.SortFunc(testIDs, naturalSort) } if err := overwriteFile(f, nil); err != nil { return nil, isDirty, err } for _, id := range testIDs { test, ok := tests[id] if !ok { continue } fmt.Fprintf(f, "\n[%s]\n%s%s\n", id, test, endSequence) } f.Close() clear(tests) testIDs = testIDs[:0] data.Reset() } return obsoleteTests, isDirty, nil } func summary( obsoleteFiles, obsoleteTests []string, NOskippedTests int, testEvents map[uint8]int, shouldUpdate bool, ) string { if len(obsoleteFiles) == 0 && len(obsoleteTests) == 0 && len(testEvents) == 0 && NOskippedTests == 0 { return "" } var s strings.Builder objectSummaryList := func(objects []string, name string) { subject := name action := "obsolete" color := colors.Yellow if len(objects) > 1 { subject = name + "s" } if shouldUpdate { action = "removed" color = colors.Green } colors.Fprint( &s, color, fmt.Sprintf("\n%s%d snapshot %s %s\n", arrowSymbol, len(objects), subject, action), ) for _, object := range objects { colors.Fprint( &s, colors.Dim, fmt.Sprintf(" %s %s%s\n", enterSymbol, bulletSymbol, object), ) } } fmt.Fprintf(&s, "\n%s\n\n", colors.Sprint(colors.BoldWhite, "Snapshot Summary")) printEvent(&s, colors.Green, successSymbol, "passed", testEvents[passed]) printEvent(&s, colors.Red, errorSymbol, "failed", testEvents[erred]) printEvent(&s, colors.Green, updateSymbol, "added", testEvents[added]) printEvent(&s, colors.Green, updateSymbol, "updated", testEvents[updated]) printEvent(&s, colors.Yellow, skipSymbol, "skipped", NOskippedTests) if len(obsoleteFiles) > 0 { objectSummaryList(obsoleteFiles, "file") } if len(obsoleteTests) > 0 { objectSummaryList(obsoleteTests, "test") } if !shouldUpdate && len(obsoleteFiles)+len(obsoleteTests) > 0 { it := "it" if len(obsoleteFiles)+len(obsoleteTests) > 1 { it = "them" } colors.Fprint( &s, colors.Dim, fmt.Sprintf( "\nTo remove %s, re-run tests with `UPDATE_SNAPS=clean go test ./...`\n", it, ), ) } return s.String() } func printEvent(w io.Writer, color, symbol, verb string, events int) { if events == 0 { return } subject := "snapshot" if events > 1 { subject += "s" } colors.Fprint(w, color, fmt.Sprintf("%s%v %s %s\n", symbol, events, subject, verb)) } func standaloneOccurrenceFMT(s string, i int) string { return fmt.Sprintf(s, i) } func snapshotOccurrenceFMT(s string, i int) string { return fmt.Sprintf("%s - %d", s, i) } // Builds a Set with all snapshot ids registered. It uses the provider formatter to build keys. func occurrences(tests map[string]int, count int, formatter func(string, int) string) set { result := make(set, len(tests)) for testID, counter := range tests { // divide a test's counter by count (how many times the go test suite ran) // this gives us how many snapshots were created in a single test run. counter = counter / count if counter > 1 { for i := 1; i <= counter; i++ { result[formatter(testID, i)] = struct{}{} } } result[formatter(testID, counter)] = struct{}{} } return result } // naturalSort is a function that can be used to sort strings in natural order func naturalSort(a, b string) int { if a == b { return 0 } if natural.Less(a, b) { return -1 } return 1 } go-snaps-0.5.16/snaps/clean_test.go000066400000000000000000000346631511036776100171510ustar00rootroot00000000000000package snaps import ( "errors" "fmt" "os" "path/filepath" "slices" "sort" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) // loadMockSnap loads a mock snap from the testdata directory func loadMockSnap(t *testing.T, name string) []byte { t.Helper() snap, err := os.ReadFile(fmt.Sprintf("testdata/%s", name)) if err != nil { t.Fatal(err) } return snap } func setupTempExamineFiles( t *testing.T, mockSnap1, mockSnap2 []byte, ) (map[string]map[string]int, string, string) { t.Helper() dir1 := t.TempDir() dir2 := t.TempDir() files := []struct { name string data []byte }{ { name: filepath.FromSlash(dir1 + "/test1.snap"), data: mockSnap1, }, { name: filepath.FromSlash(dir2 + "/test2.snap"), data: mockSnap2, }, { name: filepath.FromSlash(dir1 + "/obsolete1.snap"), data: []byte{}, }, { name: filepath.FromSlash(dir2 + "/obsolete2.snap"), data: []byte{}, }, { name: filepath.FromSlash(dir2 + "/should_not_delete.txt"), data: []byte{}, }, { name: filepath.FromSlash(dir1 + "TestSomething_my_test_1.snap"), data: []byte{}, }, { name: filepath.FromSlash(dir1 + "TestSomething_my_test_2.snap"), data: []byte{}, }, { name: filepath.FromSlash(dir1 + "TestSomething_my_test_3.snap"), data: []byte{}, }, { name: filepath.FromSlash(dir2 + "TestAnotherThing_my_test_1.snap"), data: []byte{}, }, { name: filepath.FromSlash(dir2 + "TestAnotherThing_my_simple_test_1.snap"), data: []byte{}, }, { name: filepath.FromSlash(dir2 + "TestAnotherThing_my_simple_test_2.snap"), data: []byte{}, }, } for _, file := range files { err := os.WriteFile(file.name, file.data, os.ModePerm) if err != nil { t.Fatal(err) } } tests := map[string]map[string]int{ files[0].name: { "TestDir1_1/TestSimple": 1, "TestDir1_2/TestSimple": 1, "TestDir1_3/TestSimple": 2, "TestCat": 1, "TestAlpha": 2, "TestBeta": 1, }, files[1].name: { "TestDir2_1/TestSimple": 3, "TestDir2_2/TestSimple": 1, "TestCat": 1, "TestAlpha": 2, "TestBeta": 1, }, } return tests, dir1, dir2 } func TestExamineFiles(t *testing.T) { t.Run("should parse files", func(t *testing.T) { tests, dir1, dir2 := setupTempExamineFiles( t, loadMockSnap(t, "mock-snap-1"), loadMockSnap(t, "mock-snap-2"), ) obsolete, used, isDirty := examineFiles(tests, set{ dir1 + "TestSomething_my_test_1.snap": struct{}{}, dir2 + "TestAnotherThing_my_simple_test_1.snap": struct{}{}, }, "", false) obsoleteExpected := []string{ filepath.FromSlash(dir1 + "/obsolete1.snap"), filepath.FromSlash(dir2 + "/obsolete2.snap"), filepath.FromSlash(dir1 + "TestSomething_my_test_2.snap"), filepath.FromSlash(dir1 + "TestSomething_my_test_3.snap"), filepath.FromSlash(dir2 + "TestAnotherThing_my_test_1.snap"), filepath.FromSlash(dir2 + "TestAnotherThing_my_simple_test_2.snap"), } usedExpected := []string{ filepath.FromSlash(dir1 + "/test1.snap"), filepath.FromSlash(dir2 + "/test2.snap"), } // Parse files uses maps so order of strings cannot be guaranteed sort.Strings(obsoleteExpected) sort.Strings(usedExpected) sort.Strings(obsolete) sort.Strings(used) test.Equal(t, obsoleteExpected, obsolete) test.Equal(t, usedExpected, used) test.True(t, isDirty) }) t.Run("should remove outdated files", func(t *testing.T) { shouldUpdate := true tests, dir1, dir2 := setupTempExamineFiles( t, loadMockSnap(t, "mock-snap-1"), loadMockSnap(t, "mock-snap-2"), ) examineFiles(tests, set{ dir1 + "TestSomething_my_test_1.snap": struct{}{}, dir2 + "TestAnotherThing_my_simple_test_1.snap": struct{}{}, }, "", shouldUpdate) for _, obsoleteFilename := range []string{ dir1 + "/obsolete1.snap", dir2 + "/obsolete2.snap", dir1 + "TestSomething_my_test_2.snap", dir1 + "TestSomething_my_test_3.snap", dir2 + "TestAnotherThing_my_test_1.snap", dir2 + "TestAnotherThing_my_simple_test_2.snap", } { if _, err := os.Stat(filepath.FromSlash(obsoleteFilename)); !errors.Is( err, os.ErrNotExist, ) { t.Errorf("obsolete file %s not removed", obsoleteFilename) } } }) } func TestExamineSnaps(t *testing.T) { t.Run("should report no obsolete snapshots", func(t *testing.T) { shouldUpdate, sort := false, false tests, dir1, dir2 := setupTempExamineFiles( t, loadMockSnap(t, "mock-snap-1"), loadMockSnap(t, "mock-snap-2"), ) used := []string{ filepath.FromSlash(dir1 + "/test1.snap"), filepath.FromSlash(dir2 + "/test2.snap"), } obsolete, isDirty, err := examineSnaps(tests, used, "", 1, shouldUpdate, sort) test.Equal(t, []string{}, obsolete) test.NoError(t, err) test.False(t, isDirty) }) t.Run("should report two obsolete snapshots and not change content", func(t *testing.T) { shouldUpdate, sort := false, false mockSnap1 := loadMockSnap(t, "mock-snap-1") mockSnap2 := loadMockSnap(t, "mock-snap-2") tests, dir1, dir2 := setupTempExamineFiles(t, mockSnap1, mockSnap2) used := []string{ filepath.FromSlash(dir1 + "/test1.snap"), filepath.FromSlash(dir2 + "/test2.snap"), } // Reducing test occurrence to 1 meaning the second test was removed ( testid - 2 ) tests[used[0]]["TestDir1_3/TestSimple"] = 1 // Removing the test entirely delete(tests[used[1]], "TestDir2_2/TestSimple") obsolete, isDirty, err := examineSnaps(tests, used, "", 1, shouldUpdate, sort) content1 := test.GetFileContent(t, used[0]) content2 := test.GetFileContent(t, used[1]) test.Equal(t, []string{"TestDir1_3/TestSimple - 2", "TestDir2_2/TestSimple - 1"}, obsolete) test.NoError(t, err) // Content of snaps is not changed test.Equal(t, mockSnap1, []byte(content1)) test.Equal(t, mockSnap2, []byte(content2)) // And thus we are dirty since the contents do need changing test.True(t, isDirty) }) t.Run("should update the obsolete snap files", func(t *testing.T) { shouldUpdate, sort := true, false tests, dir1, dir2 := setupTempExamineFiles( t, loadMockSnap(t, "mock-snap-1"), loadMockSnap(t, "mock-snap-2"), ) used := []string{ filepath.FromSlash(dir1 + "/test1.snap"), filepath.FromSlash(dir2 + "/test2.snap"), } // removing tests from the map means those tests are no longer used delete(tests[used[0]], "TestDir1_3/TestSimple") delete(tests[used[1]], "TestDir2_1/TestSimple") obsolete, isDirty, err := examineSnaps(tests, used, "", 1, shouldUpdate, sort) content1 := test.GetFileContent(t, used[0]) content2 := test.GetFileContent(t, used[1]) // !!unsorted expected1 := ` [TestDir1_2/TestSimple - 1] int(10) string hello world 1 2 1 --- [TestDir1_1/TestSimple - 1] int(1) string hello world 1 1 1 --- ` expected2 := ` [TestDir2_2/TestSimple - 1] int(1000) string hello world 2 2 1 --- ` test.Equal(t, []string{ "TestDir1_3/TestSimple - 1", "TestDir1_3/TestSimple - 2", "TestDir2_1/TestSimple - 1", "TestDir2_1/TestSimple - 3", "TestDir2_1/TestSimple - 2", }, obsolete, ) test.NoError(t, err) // Content of snaps have been updated test.Equal(t, expected1, content1) test.Equal(t, expected2, content2) // And thus we are not dirty test.False(t, isDirty) }) t.Run("should sort all tests when allowed to update", func(t *testing.T) { shouldUpdate, sort := true, true mockSnap1 := loadMockSnap(t, "mock-snap-sort-1") mockSnap2 := loadMockSnap(t, "mock-snap-sort-2") expectedMockSnap1 := loadMockSnap(t, "mock-snap-sort-1-sorted") expectedMockSnap2 := loadMockSnap(t, "mock-snap-sort-2-sorted") tests, dir1, dir2 := setupTempExamineFiles( t, mockSnap1, mockSnap2, ) used := []string{ filepath.FromSlash(dir1 + "/test1.snap"), filepath.FromSlash(dir2 + "/test2.snap"), } obsolete, isDirty, err := examineSnaps(tests, used, "", 1, shouldUpdate, sort) test.NoError(t, err) test.Equal(t, 0, len(obsolete)) content1 := test.GetFileContent(t, filepath.FromSlash(dir1+"/test1.snap")) content2 := test.GetFileContent(t, filepath.FromSlash(dir2+"/test2.snap")) // Content of snaps are now sorted test.Equal(t, string(expectedMockSnap1), content1) test.Equal(t, string(expectedMockSnap2), content2) // And thus we are not dirty test.False(t, isDirty) }) t.Run( "should not update file if snaps are already sorted and shouldUpdate=false", func(t *testing.T) { shouldUpdate, sort := false, true mockSnap1 := loadMockSnap(t, "mock-snap-sort-1-sorted") mockSnap2 := loadMockSnap(t, "mock-snap-sort-2-sorted") tests, dir1, dir2 := setupTempExamineFiles( t, mockSnap1, mockSnap2, ) used := []string{ filepath.FromSlash(dir1 + "/test1.snap"), filepath.FromSlash(dir2 + "/test2.snap"), } // removing tests from the map means those tests are no longer used delete(tests[used[0]], "TestDir1_3/TestSimple") delete(tests[used[1]], "TestDir2_1/TestSimple") obsolete, isDirty, err := examineSnaps(tests, used, "", 1, shouldUpdate, sort) test.NoError(t, err) test.Equal(t, []string{ "TestDir1_3/TestSimple - 1", "TestDir1_3/TestSimple - 2", "TestDir2_1/TestSimple - 1", "TestDir2_1/TestSimple - 2", "TestDir2_1/TestSimple - 3", }, obsolete, ) content1 := test.GetFileContent(t, filepath.FromSlash(dir1+"/test1.snap")) content2 := test.GetFileContent(t, filepath.FromSlash(dir2+"/test2.snap")) // Content of snaps is not changed test.Equal(t, string(mockSnap1), content1) test.Equal(t, string(mockSnap2), content2) // And thus we are dirty, since there are obsolete snapshots that should be removed test.True(t, isDirty) }, ) } func TestOccurrences(t *testing.T) { t.Run("when count 1", func(t *testing.T) { tests := map[string]int{ "add_%d": 3, "subtract_%d": 1, "divide_%d": 2, } expected := set{ "add_%d - 1": {}, "add_%d - 2": {}, "add_%d - 3": {}, "subtract_%d - 1": {}, "divide_%d - 1": {}, "divide_%d - 2": {}, } expectedStandalone := set{ "add_1": {}, "add_2": {}, "add_3": {}, "subtract_1": {}, "divide_1": {}, "divide_2": {}, } test.Equal(t, expected, occurrences(tests, 1, snapshotOccurrenceFMT)) test.Equal(t, expectedStandalone, occurrences(tests, 1, standaloneOccurrenceFMT)) }) t.Run("when count 3", func(t *testing.T) { tests := map[string]int{ "add_%d": 12, "subtract_%d": 3, "divide_%d": 9, } expected := set{ "add_%d - 1": {}, "add_%d - 2": {}, "add_%d - 3": {}, "add_%d - 4": {}, "subtract_%d - 1": {}, "divide_%d - 1": {}, "divide_%d - 2": {}, "divide_%d - 3": {}, } expectedStandalone := set{ "add_1": {}, "add_2": {}, "add_3": {}, "add_4": {}, "subtract_1": {}, "divide_1": {}, "divide_2": {}, "divide_3": {}, } test.Equal(t, expected, occurrences(tests, 3, snapshotOccurrenceFMT)) test.Equal(t, expectedStandalone, occurrences(tests, 3, standaloneOccurrenceFMT)) }) } func TestSummary(t *testing.T) { for _, v := range []struct { name string snapshot string }{ { name: "should print obsolete file", snapshot: summary([]string{"test0.snap"}, nil, 0, nil, false), }, { name: "should print obsolete tests", snapshot: summary( nil, []string{"TestMock/should_pass - 1", "TestMock/should_pass - 2"}, 0, nil, false, ), }, { name: "should print updated file", snapshot: summary([]string{"test0.snap"}, nil, 0, nil, true), }, { name: "should print updated test", snapshot: summary(nil, []string{"TestMock/should_pass - 1"}, 0, nil, true), }, { name: "should return empty string", snapshot: summary(nil, nil, 0, nil, false), }, { name: "should print events", snapshot: summary(nil, nil, 0, map[uint8]int{ added: 5, erred: 100, updated: 3, passed: 10, }, false), }, { name: "should print number of skipped tests", snapshot: summary(nil, nil, 1, nil, true), }, { name: "should print all summary", snapshot: summary( []string{"test0.snap"}, []string{"TestMock/should_pass - 1"}, 5, map[uint8]int{ added: 5, erred: 100, updated: 3, passed: 10, }, false, ), }, } { // capture v v := v t.Run(v.name, func(t *testing.T) { t.Parallel() MatchSnapshot(t, v.snapshot) }) } } func TestGetTestID(t *testing.T) { testCases := []struct { input string expectedID string valid bool }{ {"[Test/something - 10]", "Test/something - 10", true}, {input: "[Test/something - 100231231dsada]", expectedID: "", valid: false}, {input: "[Test/something - 100231231 ]", expectedID: "", valid: false}, {input: "[Test/something -100231231 ]", expectedID: "", valid: false}, {input: "[Test/something- 100231231]", expectedID: "", valid: false}, {input: "[Test/something - a ]", expectedID: "", valid: false}, {"[Test123 - Some Test]", "", false}, {"", "", false}, {"Invalid input", "", false}, {"[Test - Missing Closing Bracket", "", false}, {"[TesGetTestID- No Space]", "", false}, // must have [ {"Test something 10]", "", false}, // must have Test at the start {"TesGetTestID - ]", "", false}, // must have dash between test name and number {"[Test something 10]", "", false}, {"[Test/something - not a number]", "", false}, {"s", "", false}, } for _, tc := range testCases { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() // make sure that the capacity of b is len(tc.input), this way // indexing beyond the capacity will cause test to panic b := make([]byte, 0, len(tc.input)) b = append(b, []byte(tc.input)...) id, ok := getTestID(b) test.Equal(t, tc.valid, ok) test.Equal(t, tc.expectedID, id) }) } } func TestNaturalSort(t *testing.T) { t.Run("should sort in descending order", func(t *testing.T) { items := []string{ "[TestExample/Test_Case_1#74 - 1]", "[TestExample/Test_Case_1#05 - 1]", "[TestExample/Test_Case_1#09 - 1]", "[TestExample - 1]", "[TestExample/Test_Case_1#71 - 1]", "[TestExample/Test_Case_1#100 - 1]", "[TestExample/Test_Case_1#7 - 1]", } expected := []string{ "[TestExample - 1]", "[TestExample/Test_Case_1#05 - 1]", "[TestExample/Test_Case_1#7 - 1]", "[TestExample/Test_Case_1#09 - 1]", "[TestExample/Test_Case_1#71 - 1]", "[TestExample/Test_Case_1#74 - 1]", "[TestExample/Test_Case_1#100 - 1]", } slices.SortFunc(items, naturalSort) test.Equal(t, expected, items) }) } go-snaps-0.5.16/snaps/diff.go000066400000000000000000000136601511036776100157320ustar00rootroot00000000000000package snaps import ( "bytes" "fmt" "io" "strconv" "strings" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/gkampitakis/go-snaps/internal/difflib" "github.com/sergi/go-diff/diffmatchpatch" ) const ( diffEqual diffmatchpatch.Operation = 0 diffInsert diffmatchpatch.Operation = 1 diffDelete diffmatchpatch.Operation = -1 context = 3 ) var dmp = diffmatchpatch.New() func splitNewlines(s string) []string { lines := strings.SplitAfter(s, "\n") lines[len(lines)-1] += "\n" return lines } // isSingleline checks if a snapshot is one line or multiline. // singleline snapshots have only one newline at the end. func isSingleline(s string) bool { i := strings.Index(s, "\n") return i == len(s)-1 || i == -1 } func hasNewLine(b []byte) bool { return b[len(b)-1] == '\n' } // shouldPrintHighlights checks if the two strings are going to be presented with // inline highlights func shouldPrintHighlights(a, b string) bool { return !colors.NOCOLOR && a != "" && b != "" && isSingleline(a) && isSingleline(b) } // Compare two sequences of lines; generate the delta as a unified diff. // // Unified diffs are a compact way of showing line changes and a few // lines of context. The number of context lines is set by default to three. // // getUnifiedDiff returns a diff string along with inserted and deleted number. func getUnifiedDiff(a, b string) (string, int, int) { aLines := splitNewlines(a) bLines := splitNewlines(b) var inserted int var deleted int var s strings.Builder s.Grow(len(a) + len(b)) m := difflib.NewMatcher(aLines, bLines) for _, g := range m.GetGroupedOpCodes(context) { // aLines is a product of splitNewLines(), some items are just \"n" // if change is less than 10 items don't print the range if len(aLines) > 10 || len(bLines) > 10 { printRange(&s, g) } for _, c := range g { fallback := false i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 if c.Tag == difflib.OpReplace { expected := strings.Join(bLines[j1:j2], "") received := strings.Join(aLines[i1:i2], "") if shouldPrintHighlights(expected, received) { diff, i, d := singlelineDiff(received, expected) s.WriteString(diff) inserted += i deleted += d continue } fallback = true } if c.Tag == difflib.OpEqual { for _, line := range aLines[i1:i2] { if line == "\n" { line = newLineSymbol + "\n" } colors.FprintEqual(&s, line) } continue } // no continue, if fallback == true we want both lines printed if fallback || c.Tag == difflib.OpDelete { for _, line := range aLines[i1:i2] { colors.FprintDelete(&s, line) deleted++ } } if fallback || c.Tag == difflib.OpInsert { for _, line := range bLines[j1:j2] { colors.FprintInsert(&s, line) inserted++ } } } } return s.String(), inserted, deleted } func printRange(w io.Writer, opcodes []difflib.OpCode) { first, last := opcodes[0], opcodes[len(opcodes)-1] range1 := difflib.FormatRangeUnified(first.I1, last.I2) range2 := difflib.FormatRangeUnified(first.J1, last.J2) colors.FprintRange(w, range1, range2) } // IntPadding accepts two integers and returns two strings working as padding for aligning printed numbers // // e.g 1000 and 1 will return ā€œ and "" ( 3 spaces ) so when printed will look // // 1000 // 1 func intPadding(inserted, deleted int) (string, string) { digits := func(n int) (c int) { return len(strconv.Itoa(n)) } i := digits(inserted) d := digits(deleted) if i == d { return "", "" } diff := i - d if diff > 0 { return "", strings.Repeat(" ", diff) } return strings.Repeat(" ", -diff), "" } func singlelineDiff(expected, received string) (string, int, int) { diffs := dmp.DiffCleanupSemantic( dmp.DiffMain(expected, received, false), ) if len(diffs) == 1 && diffs[0].Type == diffEqual { return "", -1, -1 } var inserted, deleted int a := &bytes.Buffer{} b := &bytes.Buffer{} colors.FprintBg(a, colors.RedBg, colors.Reddiff, "- ") colors.FprintBg(b, colors.GreenBG, colors.Greendiff, "+ ") for _, diff := range diffs { switch diff.Type { case diffDelete: deleted++ if strings.HasSuffix(diff.Text, "\n") { colors.FprintDeleteBold(a, diff.Text[:len(diff.Text)-1]+newLineSymbol) } else { colors.FprintDeleteBold(a, diff.Text) } case diffInsert: inserted++ if strings.HasSuffix(diff.Text, "\n") { colors.FprintInsertBold(b, diff.Text[:len(diff.Text)-1]+newLineSymbol) } else { colors.FprintInsertBold(b, diff.Text) } case diffEqual: colors.FprintBg(a, colors.RedBg, colors.Reddiff, diff.Text) colors.FprintBg(b, colors.GreenBG, colors.Greendiff, diff.Text) } } if !hasNewLine(a.Bytes()) { a.WriteByte('\n') } if !hasNewLine(b.Bytes()) { b.WriteByte('\n') } a.Write(b.Bytes()) return a.String(), inserted, deleted } /* buildDiffReport creates a report with diffs it contains a header the diff body and a footer header of a diff report e.g. - Snapshot - 10 - Received + 2 body contains the diffs footer contains the relative path of snapshot e.g. at ../__snapshots__/example_test.snap:25 */ func buildDiffReport(inserted, deleted int, diff, name string, line int) string { if diff == "" { return "" } var s strings.Builder s.Grow(len(diff)) iPadding, dPadding := intPadding(inserted, deleted) s.WriteByte('\n') colors.FprintDelete(&s, fmt.Sprintf("Snapshot %s- %d\n", dPadding, deleted)) colors.FprintInsert(&s, fmt.Sprintf("Received %s+ %d\n", iPadding, inserted)) s.WriteByte('\n') s.WriteString(diff) s.WriteByte('\n') if name != "" { colors.Fprint(&s, colors.Dim, fmt.Sprintf("at %s:%d\n", name, line)) } return s.String() } func prettyDiff(expected, received, name string, line int) string { if expected == received { return "" } differ := getUnifiedDiff if shouldPrintHighlights(expected, received) { differ = singlelineDiff } diff, i, d := differ(expected, received) return buildDiffReport(i, d, diff, name, line) } go-snaps-0.5.16/snaps/diff_test.go000066400000000000000000000125651511036776100167740ustar00rootroot00000000000000package snaps import ( "strings" "testing" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/gkampitakis/go-snaps/internal/test" ) var a = `Proin justo libero, pellentesque sit amet scelerisque ut, sollicitudin non tortor. Sed velit elit, accumsan sed porttitor nec, elementum quis sapien. Phasellus mattis purus in dui pretium, eu euismod metus feugiat. Morbi turpis tellus, tincidunt mollis rutrum at, mattis laoreet lacus. Donec in quam tempus, eleifend erat sit amet, aliquet metus. Sed ullamcorper velit a est efficitur, et tempus ante rhoncus. Aliquam diam sapien, vulputate sit amet elit sit amet, commodo eleifend sapien. Donec consequat at nibh id mattis. Quisque vitae sagittis eros, convallis consectetur ante. Duis finibus suscipit mi sed consectetur. Nulla libero neque, sagittis vel nulla vel, vestibulum sagittis mauris. Ut laoreet urna lectus. Sed lorem felis, condimentum eget vehicula non, sagittis sit amet diam. Vivamus ut sapien at erat imperdiet suscipit id a lectus.` var b = `Proin justo libero, pellentesque sit amet scelerisque ut, sollicitudin non tortor. Sed velit elit, accumsan sed Ipsum nec, elementum quis sapien. Phasellus mattis purus in dui pretium, eu euismod metus feugiat. Morbi turpis Lorem, tincidunt mollis rutrum at, mattis laoreet lacus. Donec in quam tempus, eleifend erat sit amet, aliquet metus. Sed ullamcorper velit a est efficitur, et tempus ante rhoncus. Aliquam diam sapien, vulputate sit amet elit sit amet, commodo eleifend sapien. Donec consequat at nibh id mattis. Quisque vitae sagittis eros, convallis consectetur ante. Duis finibus suscipit mi sed consectetur. Nulla libero neque, sagittis vel nulla vel, vestibulum sagittis mauris. Ut laoreet urna lectus. Sed lorem felis, condimentum eget vehicula non, sagittis sit amet diam. Vivamus ut sapien at erat imperdiet suscipit id a lectus. Another Line added.` func TestStringUtils(t *testing.T) { t.Run("splitNewlines", func(t *testing.T) { for _, v := range []struct { input string expected []string }{ {"foo", []string{"foo\n"}}, {"foo\nbar", []string{"foo\n", "bar\n"}}, {"foo\nbar\n", []string{"foo\n", "bar\n", "\n"}}, {`abc efg hello \n world`, []string{"abc\n", "\t\t\tefg\n", "\t\t\thello \\n world\n"}}, } { v := v t.Run(v.input, func(t *testing.T) { t.Parallel() test.Equal(t, v.expected, splitNewlines(v.input)) }) } }) t.Run("isSingleLine", func(t *testing.T) { test.True(t, isSingleline("hello world")) test.True(t, isSingleline("hello world\n")) test.False(t, isSingleline(`hello world `)) test.False(t, isSingleline("hello \n world\n")) test.False(t, isSingleline("hello \n world")) }) } func TestDiff(t *testing.T) { t.Run("should return empty string if no diffs", func(t *testing.T) { t.Run("single line", func(t *testing.T) { expected, received := "Hello World\n", "Hello World\n" diff, deleted, inserted := singlelineDiff(expected, received) test.Equal(t, "", diff) test.Equal(t, -1, deleted) test.Equal(t, -1, inserted) test.Equal(t, "", prettyDiff(expected, received, "", -1)) }) t.Run("multiline", func(t *testing.T) { expected := `one snapshot containing new lines ` received := expected if diff := prettyDiff(expected, received, "", -1); diff != "" { t.Errorf("found diff between same string %s", diff) } }) }) t.Run("should build diff report consistently", func(t *testing.T) { MatchSnapshot(t, buildDiffReport(10000, 20, "mock-diff", "snap/path", 10)) MatchSnapshot(t, buildDiffReport(20, 10000, "mock-diff", "snap/path", 20)) }) t.Run("should not print diff report if no diffs", func(t *testing.T) { test.Equal(t, "", buildDiffReport(0, 0, "", "", -1)) }) t.Run("should not print snapshot line if not provided", func(t *testing.T) { MatchSnapshot(t, buildDiffReport(10, 2, "there is a diff here", "", -1)) }) t.Run("with color", func(t *testing.T) { colors.NOCOLOR = false t.Run("should apply highlights on single line diff", func(t *testing.T) { a := strings.Repeat("abcd", 20) b := strings.Repeat("abcf", 20) MatchSnapshot(t, prettyDiff(a, b, "snap/path", 10)) }) t.Run("multiline diff", func(t *testing.T) { MatchSnapshot(t, prettyDiff(a, b, "snap/path", 10)) }) }) t.Run("no color", func(t *testing.T) { t.Cleanup(func() { colors.NOCOLOR = false }) colors.NOCOLOR = true t.Run("should apply highlights on single line diff", func(t *testing.T) { a := strings.Repeat("abcd", 20) b := strings.Repeat("abcf", 20) d := prettyDiff(a, b, "snap/path", 10) MatchSnapshot(t, d) }) t.Run("multiline diff", func(t *testing.T) { MatchSnapshot(t, prettyDiff(a, b, "snap/path", 20)) }) }) t.Run("should print newline diffs", func(t *testing.T) { t.Run("multiline", func(t *testing.T) { a := `snapshot with multiple lines ` b := `snapshot with multiple lines diff ` MatchSnapshot(t, prettyDiff(a, b, "snap/path", 10)) MatchSnapshot(t, prettyDiff(b, a, "snap/path", 10)) }) t.Run("singleline", func(t *testing.T) { a := "single line snap" b := "single line snap \n" c := "single line snap\n" MatchSnapshot(t, prettyDiff(a, b, "snap/path", 10)) MatchSnapshot(t, prettyDiff(a, b, "snap/path", 10)) MatchSnapshot(t, prettyDiff(a, c, "snap/path", 10)) MatchSnapshot(t, prettyDiff(c, a, "snap/path", 10)) }) }) } go-snaps-0.5.16/snaps/matchInlineSnapshot.go000066400000000000000000000152221511036776100207710ustar00rootroot00000000000000package snaps import ( "errors" "fmt" "go/ast" "go/format" "go/parser" "go/token" "os" "strings" "sync" "github.com/kr/pretty" ) type inlineSnapshotsLineMapping struct { // this map keeps a file inlineSnapshot call lines mapping map[string][]int sync.RWMutex } // AddLine func (i *inlineSnapshotsLineMapping) AddLine(file string, line int) { i.Lock() defer i.Unlock() i.mapping[file] = append(i.mapping[file], line) } func (i *inlineSnapshotsLineMapping) AddFileIfNotExists(file string) bool { i.Lock() defer i.Unlock() if _, exists := i.mapping[file]; exists { return false } i.mapping[file] = make([]int, 0) return true } func (i *inlineSnapshotsLineMapping) GetLine(file string, index int) int { i.RLock() defer i.RUnlock() // this should not happen, returning -1 means we ignore it. if index >= len(i.mapping[file]) { return -1 } line := i.mapping[file][index] return line } type inlineSnapshot *string var ( inlineSnapshotLineMapping = inlineSnapshotsLineMapping{ mapping: make(map[string][]int), RWMutex: sync.RWMutex{}, } errLocateCall = errors.New("cannot locate MatchInlineSnapshot call") ) // Inline representation of snapshot func Inline(s string) inlineSnapshot { return &s } /* MatchInlineSnapshot compares a test value against an expected "inline snapshot" value. Usage: 1. On the first run, call with nil as the expected value: MatchInlineSnapshot(t, "mysnapshot", nil) This will record the current value as the snapshot. 2. On subsequent runs, call with the snapshot value: MatchInlineSnapshot(t, "mysnapshot", snaps.Inline("mysnapshot")) This will verify that the value matches the stored snapshot. An "inline snapshot" is a literal value stored directly in your test code, making it easy to update and review expected outputs. */ func (c *Config) MatchInlineSnapshot(t testingT, received any, inlineSnap inlineSnapshot) { t.Helper() matchInlineSnapshot(c, t, received, inlineSnap) } /* MatchInlineSnapshot compares a test value against an expected "inline snapshot" value. Usage: 1. On the first run, call with nil as the expected value: MatchInlineSnapshot(t, "mysnapshot", nil) This will record the current value as the snapshot. 2. On subsequent runs, call with the snapshot value: MatchInlineSnapshot(t, "mysnapshot", snaps.Inline("mysnapshot")) This will verify that the value matches the stored snapshot. An "inline snapshot" is a literal value stored directly in your test code, making it easy to update and review expected outputs. */ func MatchInlineSnapshot(t testingT, received any, inlineSnap inlineSnapshot) { t.Helper() matchInlineSnapshot(&defaultConfig, t, received, inlineSnap) } func matchInlineSnapshot(c *Config, t testingT, received any, inlineSnap inlineSnapshot) { t.Helper() snapshot := pretty.Sprint(received) filename, line := baseCaller(1) // we should only register call positions if we are modifying the file and the file hasn't been registered yet. if (inlineSnap == nil || shouldUpdate(c.update)) && inlineSnapshotLineMapping.AddFileIfNotExists(filename) { if err := registerInlineCallIdx(filename); err != nil { handleError(t, err) return } } if inlineSnap == nil { if !shouldCreate(c.update) { handleError(t, errSnapNotFound) return } if err := upsertInlineSnapshot(filename, line, snapshot); err != nil { handleError(t, err) return } t.Log(addedMsg) testEvents.register(added) return } diff := prettyDiff(*inlineSnap, snapshot, "", -1) if diff == "" { testEvents.register(passed) return } if !shouldUpdate(c.update) { handleError(t, diff) return } if err := upsertInlineSnapshot(filename, line, snapshot); err != nil { handleError(t, err) return } t.Log(updatedMsg) testEvents.register(updated) } func upsertInlineSnapshot(filename string, callerLine int, snapshot string) error { inlineSnapshotIdx := 0 traverseError := errLocateCall fset, astFile, err := parseFileAst(filename) if err != nil { return err } traverseMatchInlineSnapshotAst(astFile, func(ce *ast.CallExpr) bool { if line := inlineSnapshotLineMapping.GetLine(filename, inlineSnapshotIdx); line == callerLine { ce.Args[2] = createInlineArgument(snapshot) // reset error as we found the caller traverseError = nil return false } inlineSnapshotIdx++ // continue searching return true }) if traverseError != nil { return traverseError } // Validate AST before writing var buf strings.Builder if err := format.Node(&buf, fset, astFile); err != nil { return fmt.Errorf( "invalid AST generated (snapshot contains problematic characters like backticks): %w", err, ) } // Write to file file, err := os.OpenFile(filename, os.O_TRUNC|os.O_WRONLY, os.ModePerm) if err != nil { return err } defer file.Close() if _, err := file.WriteString(buf.String()); err != nil { return err } return nil } // registerInlineCallIdx is expected to be called once per file and before getting modified // it parses the file and registers all MatchInlineSnapshot call line numbers func registerInlineCallIdx(filename string) error { fset, astFile, err := parseFileAst(filename) if err != nil { return err } traverseMatchInlineSnapshotAst(astFile, func(ce *ast.CallExpr) bool { inlineSnapshotLineMapping.AddLine(filename, fset.Position(ce.Pos()).Line) return true }) return nil } /* AST Code */ func createInlineArgument(s string) ast.Expr { v := fmt.Sprintf("`%s`", s) if isSingleline(s) { v = fmt.Sprintf("%q", s) } return &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{Name: "snaps"}, Sel: &ast.Ident{Name: "Inline"}, }, Args: []ast.Expr{&ast.BasicLit{ Kind: token.STRING, Value: v, }}, } } func traverseMatchInlineSnapshotAst(astFile *ast.File, fn func(*ast.CallExpr) bool) { breakEarly := false for _, decl := range astFile.Decls { if breakEarly { return } funcDecl, ok := decl.(*ast.FuncDecl) if !ok { continue } if !strings.HasPrefix(funcDecl.Name.Name, "Test") { continue } ast.Inspect(decl, func(n ast.Node) bool { if breakEarly { return false } callExpr, ok := n.(*ast.CallExpr) if !ok { return true } selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr) if !ok { return true } if selectorExpr.Sel.Name == "MatchInlineSnapshot" { if !fn(callExpr) { breakEarly = true return false } } return true }) } } func parseFileAst(filename string) (*token.FileSet, *ast.File, error) { fileSet := token.NewFileSet() astFile, err := parser.ParseFile( fileSet, filename, nil, parser.ParseComments|parser.SkipObjectResolution, ) if err != nil { return nil, nil, err } return fileSet, astFile, err } go-snaps-0.5.16/snaps/matchInlineSnapshot_test.go000066400000000000000000000014341511036776100220300ustar00rootroot00000000000000package snaps import ( "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestMatchInlineSnapshot(t *testing.T) { t.Run("should error in case of different input from inline snapshot", func(t *testing.T) { mockT := test.NewMockTestingT(t) mockT.MockError = func(a ...any) { test.Equal( t, "\n\x1b[38;5;52m\x1b[48;5;225m- Snapshot - 1\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m+ Received + 0\x1b[0m\n\n\x1b[48;5;225m\x1b"+ "[38;5;52m- \x1b[0m\x1b[48;5;127m\x1b[38;5;255mdifferent \x1b[0m\x1b[48;5;225m\x1b[38;5;52mvalue\x1b[0m\n\x1b[48;5;159m\x1b"+ "[38;5;22m+ \x1b[0m\x1b[48;5;159m\x1b[38;5;22mvalue\x1b[0m\n\n", a[0].(string), ) } MatchInlineSnapshot(mockT, "value", Inline("different value")) test.Equal(t, 1, testEvents.items[erred]) }) } go-snaps-0.5.16/snaps/matchJSON.go000066400000000000000000000101521511036776100166010ustar00rootroot00000000000000package snaps import ( "encoding/json" "errors" "fmt" "strings" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/gkampitakis/go-snaps/match" "github.com/tidwall/gjson" "github.com/tidwall/pretty" ) var ( defaultPrettyJSONOptions = &pretty.Options{ SortKeys: true, Indent: " ", } errInvalidJSON = errors.New("invalid json") ) /* MatchJSON verifies the input matches the most recent snap file. Input can be a valid json string or []byte or whatever value can be passed successfully on `json.Marshal`. snaps.MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`) snaps.MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`)) snaps.MatchJSON(t, User{10, "mock-email"}) MatchJSON also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchJSON(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("created")) */ func (c *Config) MatchJSON(t testingT, input any, matchers ...match.JSONMatcher) { t.Helper() matchJSON(c, t, input, matchers...) } /* MatchJSON verifies the input matches the most recent snap file. Input can be a valid json string or []byte or whatever value can be passed successfully on `json.Marshal`. snaps.MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`) snaps.MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`)) snaps.MatchJSON(t, User{10, "mock-email"}) MatchJSON also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchJSON(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("created")) */ func MatchJSON(t testingT, input any, matchers ...match.JSONMatcher) { t.Helper() matchJSON(&defaultConfig, t, input, matchers...) } func matchJSON(c *Config, t testingT, input any, matchers ...match.JSONMatcher) { t.Helper() snapPath, snapPathRel := snapshotPath(c, t.Name(), false) testID := testsRegistry.getTestID(snapPath, t.Name()) t.Cleanup(func() { testsRegistry.reset(snapPath, t.Name()) }) j, err := validateJSON(input) if err != nil { handleError(t, err) return } j, matchersErrors := applyJSONMatchers(j, matchers...) if len(matchersErrors) > 0 { s := strings.Builder{} for _, err := range matchersErrors { colors.Fprint( &s, colors.Red, fmt.Sprintf( "\n%smatch.%s(\"%s\") - %s", errorSymbol, err.Matcher, err.Path, err.Reason, ), ) } handleError(t, s.String()) return } snapshot := takeJSONSnapshot(c, j) prevSnapshot, line, err := getPrevSnapshot(testID, snapPath) if errors.Is(err, errSnapNotFound) { if !shouldCreate(c.update) { handleError(t, err) return } err := addNewSnapshot(testID, snapshot, snapPath) if err != nil { handleError(t, err) return } t.Log(addedMsg) testEvents.register(added) return } if err != nil { handleError(t, err) return } diff := prettyDiff(prevSnapshot, snapshot, snapPathRel, line) if diff == "" { testEvents.register(passed) return } if !shouldUpdate(c.update) { handleError(t, diff) return } if err = updateSnapshot(testID, snapshot, snapPath); err != nil { handleError(t, err) return } t.Log(updatedMsg) testEvents.register(updated) } func validateJSON(input any) ([]byte, error) { switch j := input.(type) { case string: if !gjson.Valid(j) { return nil, errInvalidJSON } return []byte(j), nil case []byte: if !gjson.ValidBytes(j) { return nil, errInvalidJSON } return j, nil default: return json.Marshal(input) } } func takeJSONSnapshot(c *Config, b []byte) string { return strings.TrimSuffix(string(pretty.PrettyOptions(b, c.json.getPrettyJSONOptions())), "\n") } func applyJSONMatchers(b []byte, matchers ...match.JSONMatcher) ([]byte, []match.MatcherError) { errors := []match.MatcherError{} for _, m := range matchers { json, errs := m.JSON(b) if len(errs) > 0 { errors = append(errors, errs...) continue } b = json } return b, errors } go-snaps-0.5.16/snaps/matchJSON_test.go000066400000000000000000000145261511036776100176510ustar00rootroot00000000000000package snaps import ( "errors" "testing" "github.com/gkampitakis/go-snaps/internal/test" "github.com/gkampitakis/go-snaps/match" ) const jsonFilename = "matchJSON_test.snap" func TestMatchJSON(t *testing.T) { t.Run("should create json snapshot", func(t *testing.T) { expected := `{ "items": [ 5, 1, 3, 4 ], "user": "mock-name" }` for _, tc := range []struct { name string input any }{ { name: "string", input: `{"user":"mock-name","items":[5,1,3,4]}`, }, { name: "byte", input: []byte(`{"user":"mock-name","items":[5,1,3,4]}`), }, { name: "marshal object", input: struct { User string `json:"user"` Items []int `json:"items"` }{ User: "mock-name", Items: []int{5, 1, 3, 4}, }, }, } { t.Run(tc.name, func(t *testing.T) { snapPath := setupSnapshot(t, jsonFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } MatchJSON(mockT, tc.input) snap, line, err := getPrevSnapshot("[mock-name - 1]", snapPath) test.NoError(t, err) test.Equal(t, 2, line) test.Equal(t, expected, snap) test.Equal(t, 1, testEvents.items[added]) // clean up function called test.Equal(t, 0, testsRegistry.running[snapPath]["mock-name"]) test.Equal(t, 1, testsRegistry.cleanup[snapPath]["mock-name"]) }) } }) t.Run("should validate json", func(t *testing.T) { for _, tc := range []struct { name string input any err string }{ { name: "string", input: "", err: "invalid json", }, { name: "byte", input: []byte(`{"user"`), err: "invalid json", }, { name: "struct", input: make(chan struct{}), err: "json: unsupported type: chan struct {}", }, } { t.Run(tc.name, func(t *testing.T) { setupSnapshot(t, jsonFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, tc.err, (args[0].(error)).Error()) } MatchJSON(mockT, tc.input) }) } }) t.Run("matchers", func(t *testing.T) { t.Run("should apply matchers in order", func(t *testing.T) { snapPath := setupSnapshot(t, jsonFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } c1 := func(val any) (any, error) { return map[string]any{"key2": nil}, nil } c2 := func(val any) (any, error) { return map[string]any{"key3": nil}, nil } c3 := func(val any) (any, error) { return map[string]any{"key4": nil}, nil } MatchJSON( mockT, `{"key1":""}`, match.Custom("key1", c1), match.Custom("key1.key2", c2), match.Custom("key1.key2.key3", c3), ) test.Equal( t, "\n[mock-name - 1]\n{\n \"key1\": {\n \"key2\": {\n \"key3\": {\n \"key4\": null\n }\n }\n }\n}\n---\n", test.GetFileContent(t, snapPath), ) }) t.Run("should aggregate errors from matchers", func(t *testing.T) { setupSnapshot(t, jsonFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, "\x1b[31;1m\nāœ• match.Custom(\"age\") - mock error"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"missing.key.1\") - path does not exist"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"missing.key.2\") - path does not exist\x1b[0m", args[0], ) } c := func(val any) (any, error) { return nil, errors.New("mock error") } MatchJSON( mockT, `{"age":10}`, match.Custom("age", c), match.Any("missing.key.1", "missing.key.2"), ) }) }) t.Run("if it's running on ci should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, jsonFilename, true) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } MatchJSON(mockT, "{}") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("if snaps.Update(false) should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, jsonFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } WithConfig(Update(false)).MatchJSON(mockT, "{}") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("should update snapshot when 'shouldUpdate'", func(t *testing.T) { snapPath := setupSnapshot(t, jsonFilename, false, "true") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchJSON(mockT, "{\"value\":\"hello world\"}") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params MatchJSON(mockT, "{\"value\":\"bye world\"}") test.Equal( t, "\n[mock-name - 1]\n{\n \"value\": \"bye world\"\n}\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }) t.Run( "should create and update snapshot when UPDATE_SNAPS=always even on CI", func(t *testing.T) { snapPath := setupSnapshot(t, jsonFilename, true, "always") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot WithConfig(Update(false)).MatchJSON(mockT, "{\"value\":\"hello world\"}") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params WithConfig(Update(false)).MatchJSON(mockT, "{\"value\":\"bye world\"}") test.Equal( t, "\n[mock-name - 1]\n{\n \"value\": \"bye world\"\n}\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }, ) } go-snaps-0.5.16/snaps/matchSnapshot.go000066400000000000000000000044751511036776100176420ustar00rootroot00000000000000package snaps import ( "errors" "strings" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/kr/pretty" ) /* MatchSnapshot verifies the values match the most recent snap file You can pass multiple values MatchSnapshot(t, 10, "hello world") or call MatchSnapshot multiples times inside a test MatchSnapshot(t, 10) MatchSnapshot(t, "hello world") The difference is the latter will create multiple entries. */ func (c *Config) MatchSnapshot(t testingT, values ...any) { t.Helper() matchSnapshot(c, t, values...) } /* MatchSnapshot verifies the values match the most recent snap file You can pass multiple values MatchSnapshot(t, 10, "hello world") or call MatchSnapshot multiples times inside a test MatchSnapshot(t, 10) MatchSnapshot(t, "hello world") The difference is the latter will create multiple entries. */ func MatchSnapshot(t testingT, values ...any) { t.Helper() matchSnapshot(&defaultConfig, t, values...) } func matchSnapshot(c *Config, t testingT, values ...any) { t.Helper() if len(values) == 0 { t.Log(colors.Sprint(colors.Yellow, "[warning] MatchSnapshot call without params\n")) return } snapPath, snapPathRel := snapshotPath(c, t.Name(), false) testID := testsRegistry.getTestID(snapPath, t.Name()) t.Cleanup(func() { testsRegistry.reset(snapPath, t.Name()) }) snapshot := takeSnapshot(values) prevSnapshot, line, err := getPrevSnapshot(testID, snapPath) if errors.Is(err, errSnapNotFound) { if !shouldCreate(c.update) { handleError(t, err) return } err := addNewSnapshot(testID, snapshot, snapPath) if err != nil { handleError(t, err) return } t.Log(addedMsg) testEvents.register(added) return } if err != nil { handleError(t, err) return } diff := prettyDiff( unescapeEndChars(prevSnapshot), unescapeEndChars(snapshot), snapPathRel, line, ) if diff == "" { testEvents.register(passed) return } if !shouldUpdate(c.update) { handleError(t, diff) return } if err = updateSnapshot(testID, snapshot, snapPath); err != nil { handleError(t, err) return } t.Log(updatedMsg) testEvents.register(updated) } func takeSnapshot(objects []any) string { snapshots := make([]string, len(objects)) for i, object := range objects { snapshots[i] = pretty.Sprint(object) } return escapeEndChars(strings.Join(snapshots, "\n")) } go-snaps-0.5.16/snaps/matchSnapshot_test.go000066400000000000000000000213141511036776100206700ustar00rootroot00000000000000package snaps import ( "errors" "os" "path/filepath" "testing" "github.com/gkampitakis/ciinfo" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/gkampitakis/go-snaps/internal/test" ) const ( fileName = "matchSnapshot_test.snap" mockSnap = ` [Test_1/TestSimple - 1] int(1) string hello world 1 1 1 --- [Test_3/TestSimple - 1] int(100) string hello world 1 3 1 --- [Test_3/TestSimple - 2] int(1000) string hello world 1 3 2 --- ` ) func setupSnapshot(t *testing.T, file string, ci bool, update ...string) string { t.Helper() dir, _ := os.Getwd() snapPath := filepath.Join(dir, "__snapshots__", file) isCI = ci updateVARPrev := updateVAR updateVAR = "" if len(update) > 0 { updateVAR = update[0] } t.Cleanup(func() { os.Remove(snapPath) testsRegistry = newRegistry() standaloneTestsRegistry = newStandaloneRegistry() testEvents = newTestEvents() isCI = ciinfo.IsCI updateVAR = updateVARPrev }) _, err := os.Stat(snapPath) // This is for checking we are starting with a clean state testing test.True(t, errors.Is(err, os.ErrNotExist)) return snapPath } func TestMatchSnapshot(t *testing.T) { t.Run("should create snapshot", func(t *testing.T) { snapPath := setupSnapshot(t, fileName, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } MatchSnapshot(mockT, 10, "hello world") test.Equal( t, "\n[mock-name - 1]\nint(10)\nhello world\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[added]) // clean up function called test.Equal(t, 0, testsRegistry.running[snapPath]["mock-name"]) test.Equal(t, 1, testsRegistry.cleanup[snapPath]["mock-name"]) }) t.Run("if it's running on ci should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, fileName, true) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } MatchSnapshot(mockT, 10, "hello world") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("if snaps.Update(false) should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, fileName, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } WithConfig(Update(false)).MatchSnapshot(mockT, 10, "hello world") test.Equal(t, 1, testEvents.items[erred]) }) t.Run( "should create and update snapshot when UPDATE_SNAPS=always even on CI", func(t *testing.T) { snapPath := setupSnapshot(t, fileName, true, "always") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, func(received any) { t.Error("should not be called 3rd time") }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot WithConfig(Update((false))).MatchSnapshot(mockT, 10, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params WithConfig(Update((false))).MatchSnapshot(mockT, 100, "bye world") test.Equal( t, "\n[mock-name - 1]\nint(100)\nbye world\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }, ) t.Run("should return error when diff is found", func(t *testing.T) { setupSnapshot(t, fileName, false) printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { t.Error("should not be called 2nd time") }, } mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { expected := "\n\x1b[38;5;52m\x1b[48;5;225m- Snapshot - 2\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m" + "+ Received + 2\x1b[0m\n\n\x1b[38;5;52m\x1b[48;5;225m- int(10)\x1b[0m\n\x1b[38;5;52m\x1b[48;5;225m" + "- hello world\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m+ int(100)\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m" + "+ bye world\x1b[0m\n\n\x1b[2mat " + filepath.FromSlash( "__snapshots__/matchSnapshot_test.snap:2", ) + "\n\x1b[0m" test.Equal(t, expected, args[0].(string)) } mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchSnapshot(mockT, 10, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params MatchSnapshot(mockT, 100, "bye world") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("should update snapshot", func(t *testing.T) { t.Run("when 'updateVAR==true'", func(t *testing.T) { snapPath := setupSnapshot(t, fileName, false, "true") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, func(received any) { t.Error("should not be called 3rd time") }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchSnapshot(mockT, 10, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params MatchSnapshot(mockT, 100, "bye world") test.Equal( t, "\n[mock-name - 1]\nint(100)\nbye world\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }) t.Run("when config update", func(t *testing.T) { snapPath := setupSnapshot(t, fileName, false, "false") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, func(received any) { t.Error("should not be called 3rd time") }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } s := WithConfig(Update(true)) // First call for creating the snapshot s.MatchSnapshot(mockT, 10, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params s.MatchSnapshot(mockT, 100, "bye world") test.Equal( t, "\n[mock-name - 1]\nint(100)\nbye world\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }) }) t.Run("should print warning if no params provided", func(t *testing.T) { mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal( t, colors.Sprint(colors.Yellow, "[warning] MatchSnapshot call without params\n"), args[0].(string), ) } MatchSnapshot(mockT) }) t.Run("diff should not print the escaped characters", func(t *testing.T) { setupSnapshot(t, fileName, false) printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { t.Error("should not be called 2nd time") }, } mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { expected := "\n\x1b[38;5;52m\x1b[48;5;225m- Snapshot - 3\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m" + "+ Received + 3\x1b[0m\n\n\x1b[38;5;52m\x1b[48;5;225m- int(10)\x1b[0m\n\x1b[38;5;52m\x1b[48;5;225m" + "- hello world----\x1b[0m\n\x1b[38;5;52m\x1b[48;5;225m- ---\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m" + "+ int(100)\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m+ bye world----\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m" + "+ --\x1b[0m\n\n\x1b[2mat " + filepath.FromSlash( "__snapshots__/matchSnapshot_test.snap:2", ) + "\n\x1b[0m" test.Equal(t, expected, args[0].(string)) } mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot ( adding ending chars inside the diff ) MatchSnapshot(mockT, 10, "hello world----", endSequence) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params MatchSnapshot(mockT, 100, "bye world----", "--") }) } go-snaps-0.5.16/snaps/matchStandaloneJSON.go000066400000000000000000000076161511036776100206250ustar00rootroot00000000000000package snaps import ( "errors" "fmt" "strings" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/gkampitakis/go-snaps/match" ) /* MatchStandaloneJSON verifies the input matches the most recent snap file. Input can be a valid json string or []byte or whatever value can be passed successfully on `json.Marshal`. snaps.MatchStandaloneJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`) snaps.MatchStandaloneJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`)) snaps.MatchStandaloneJSON(t, User{10, "mock-email"}) MatchStandaloneJSON also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchStandaloneJSON(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("created")) MatchStandaloneJSON creates one snapshot file per call. You can call MatchStandaloneJSON multiple times inside a test. It will create multiple snapshot files at `__snapshots__` folder by default. */ func (c *Config) MatchStandaloneJSON(t testingT, input any, matchers ...match.JSONMatcher) { t.Helper() if c.extension == "" { c.extension = ".json" } matchStandaloneJSON(c, t, input, matchers...) } /* MatchStandaloneJSON verifies the input matches the most recent snap file. Input can be a valid json string or []byte or whatever value can be passed successfully on `json.Marshal`. snaps.MatchStandaloneJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`) snaps.MatchStandaloneJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`)) snaps.MatchStandaloneJSON(t, User{10, "mock-email"}) MatchStandaloneJSON also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchStandaloneJSON(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("created")) MatchStandaloneJSON creates one snapshot file per call. You can call MatchStandaloneJSON multiple times inside a test. It will create multiple snapshot files at `__snapshots__` folder by default. */ func MatchStandaloneJSON(t testingT, input any, matchers ...match.JSONMatcher) { t.Helper() c := defaultConfig if c.extension == "" { c.extension = ".json" } matchStandaloneJSON(&c, t, input, matchers...) } func matchStandaloneJSON(c *Config, t testingT, input any, matchers ...match.JSONMatcher) { t.Helper() genericPathSnap, genericSnapPathRel := snapshotPath(c, t.Name(), true) snapPath, snapPathRel := standaloneTestsRegistry.getTestID(genericPathSnap, genericSnapPathRel) t.Cleanup(func() { standaloneTestsRegistry.reset(genericPathSnap) }) j, err := validateJSON(input) if err != nil { handleError(t, err) return } j, matchersErrors := applyJSONMatchers(j, matchers...) if len(matchersErrors) > 0 { s := strings.Builder{} for _, err := range matchersErrors { colors.Fprint( &s, colors.Red, fmt.Sprintf( "\n%smatch.%s(\"%s\") - %s", errorSymbol, err.Matcher, err.Path, err.Reason, ), ) } handleError(t, s.String()) return } snapshot := takeJSONSnapshot(c, j) prevSnapshot, err := getPrevStandaloneSnapshot(snapPath) if errors.Is(err, errSnapNotFound) { if !shouldCreate(c.update) { handleError(t, err) return } err := upsertStandaloneSnapshot(snapshot, snapPath) if err != nil { handleError(t, err) return } t.Log(addedMsg) testEvents.register(added) return } if err != nil { handleError(t, err) return } diff := prettyDiff( prevSnapshot, snapshot, snapPathRel, 1, ) if diff == "" { testEvents.register(passed) return } if !shouldUpdate(c.update) { handleError(t, diff) return } if err = upsertStandaloneSnapshot(snapshot, snapPath); err != nil { handleError(t, err) return } t.Log(updatedMsg) testEvents.register(updated) } go-snaps-0.5.16/snaps/matchStandaloneJSON_test.go000066400000000000000000000146411511036776100216600ustar00rootroot00000000000000package snaps import ( "errors" "testing" "github.com/gkampitakis/go-snaps/internal/test" "github.com/gkampitakis/go-snaps/match" ) const jsonStandaloneFilename = "mock-name_1.snap.json" func TestMatchStandaloneJSON(t *testing.T) { t.Run("should create json snapshot", func(t *testing.T) { expected := `{ "items": [ 5, 1, 3, 4 ], "user": "mock-name" }` for _, tc := range []struct { name string input any }{ { name: "string", input: `{"user":"mock-name","items":[5,1,3,4]}`, }, { name: "byte", input: []byte(`{"user":"mock-name","items":[5,1,3,4]}`), }, { name: "marshal object", input: struct { User string `json:"user"` Items []int `json:"items"` }{ User: "mock-name", Items: []int{5, 1, 3, 4}, }, }, } { t.Run(tc.name, func(t *testing.T) { snapPath := setupSnapshot(t, jsonStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } MatchStandaloneJSON(mockT, tc.input) snap := test.GetFileContent(t, snapPath) test.Equal(t, expected, snap) test.Equal(t, 1, testEvents.items[added]) // clean up function called for _, v := range standaloneTestsRegistry.running { test.Equal(t, 0, v) } for _, v := range standaloneTestsRegistry.cleanup { test.Equal(t, 1, v) } }) } }) t.Run("should validate json", func(t *testing.T) { for _, tc := range []struct { name string input any err string }{ { name: "string", input: "", err: "invalid json", }, { name: "byte", input: []byte(`{"user"`), err: "invalid json", }, { name: "struct", input: make(chan struct{}), err: "json: unsupported type: chan struct {}", }, } { t.Run(tc.name, func(t *testing.T) { setupSnapshot(t, jsonStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, tc.err, (args[0].(error)).Error()) } MatchStandaloneJSON(mockT, tc.input) }) } }) t.Run("matchers", func(t *testing.T) { t.Run("should apply matchers in order", func(t *testing.T) { snapPath := setupSnapshot(t, jsonStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } c1 := func(val any) (any, error) { return map[string]any{"key2": nil}, nil } c2 := func(val any) (any, error) { return map[string]any{"key3": nil}, nil } c3 := func(val any) (any, error) { return map[string]any{"key4": nil}, nil } MatchStandaloneJSON( mockT, `{"key1":""}`, match.Custom("key1", c1), match.Custom("key1.key2", c2), match.Custom("key1.key2.key3", c3), ) test.Equal( t, "{\n \"key1\": {\n \"key2\": {\n \"key3\": {\n \"key4\": null\n }\n }\n }\n}", test.GetFileContent(t, snapPath), ) }) t.Run("should aggregate errors from matchers", func(t *testing.T) { setupSnapshot(t, jsonStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, "\x1b[31;1m\nāœ• match.Custom(\"age\") - mock error"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"missing.key.1\") - path does not exist"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"missing.key.2\") - path does not exist\x1b[0m", args[0], ) } c := func(val any) (any, error) { return nil, errors.New("mock error") } MatchStandaloneJSON( mockT, `{"age":10}`, match.Custom("age", c), match.Any("missing.key.1", "missing.key.2"), ) }) }) t.Run("if it's running on ci should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, jsonStandaloneFilename, true) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } MatchStandaloneJSON(mockT, "{}") test.Equal(t, 1, testEvents.items[erred]) }) t.Run( "should create and update snapshot when UPDATE_SNAPS=always even on CI", func(t *testing.T) { snapPath := setupSnapshot(t, jsonStandaloneFilename, true, "always") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot WithConfig(Update(false)).MatchStandaloneJSON(mockT, "{\"value\":\"hello world\"}") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params WithConfig(Update(false)).MatchStandaloneJSON(mockT, "{\"value\":\"bye world\"}") test.Equal( t, "{\n \"value\": \"bye world\"\n}", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }, ) t.Run("if snaps.Update(false) should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, fileName, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } WithConfig(Update(false)).MatchStandaloneJSON(mockT, "{}") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("should update snapshot when 'shouldUpdate'", func(t *testing.T) { snapPath := setupSnapshot(t, jsonStandaloneFilename, false, "true") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchStandaloneJSON(mockT, "{\"value\":\"hello world\"}") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params MatchStandaloneJSON(mockT, "{\"value\":\"bye world\"}") test.Equal( t, "{\n \"value\": \"bye world\"\n}", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }) } go-snaps-0.5.16/snaps/matchStandaloneSnapshot.go000066400000000000000000000040171511036776100216430ustar00rootroot00000000000000package snaps import ( "errors" "github.com/kr/pretty" ) /* MatchStandaloneSnapshot verifies the input matches the most recent snap file MatchStandaloneSnapshot(t, "Hello World") MatchStandaloneSnapshot creates one snapshot file per call. You can call MatchStandaloneSnapshot multiple times inside a test. It will create multiple snapshot files at `__snapshots__` folder by default. */ func (c *Config) MatchStandaloneSnapshot(t testingT, input any) { t.Helper() matchStandaloneSnapshot(c, t, input) } /* MatchStandaloneSnapshot verifies the input matches the most recent snap file MatchStandaloneSnapshot(t, "Hello World") MatchStandaloneSnapshot creates one snapshot file per call. You can call MatchStandaloneSnapshot multiple times inside a test. It will create multiple snapshot files at `__snapshots__` folder by default. */ func MatchStandaloneSnapshot(t testingT, input any) { t.Helper() matchStandaloneSnapshot(&defaultConfig, t, input) } func matchStandaloneSnapshot(c *Config, t testingT, input any) { t.Helper() genericPathSnap, genericSnapPathRel := snapshotPath(c, t.Name(), true) snapPath, snapPathRel := standaloneTestsRegistry.getTestID(genericPathSnap, genericSnapPathRel) t.Cleanup(func() { standaloneTestsRegistry.reset(genericPathSnap) }) snapshot := pretty.Sprint(input) prevSnapshot, err := getPrevStandaloneSnapshot(snapPath) if errors.Is(err, errSnapNotFound) { if !shouldCreate(c.update) { handleError(t, err) return } err := upsertStandaloneSnapshot(snapshot, snapPath) if err != nil { handleError(t, err) return } t.Log(addedMsg) testEvents.register(added) return } if err != nil { handleError(t, err) return } diff := prettyDiff( prevSnapshot, snapshot, snapPathRel, 1, ) if diff == "" { testEvents.register(passed) return } if !shouldUpdate(c.update) { handleError(t, diff) return } if err = upsertStandaloneSnapshot(snapshot, snapPath); err != nil { handleError(t, err) return } t.Log(updatedMsg) testEvents.register(updated) } go-snaps-0.5.16/snaps/matchStandaloneSnapshot_test.go000066400000000000000000000162741511036776100227120ustar00rootroot00000000000000package snaps import ( "path/filepath" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) const standaloneFilename = "mock-name_1.snap" func TestMatchStandaloneSnapshot(t *testing.T) { t.Run("should create snapshot", func(t *testing.T) { snapPath := setupSnapshot(t, standaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, "hello world", test.GetFileContent(t, snapPath)) test.Equal(t, 1, testEvents.items[added]) // clean up function called registryKey := filepath.Join( filepath.Dir(snapPath), "mock-name_%d.snap", ) test.Equal(t, 0, standaloneTestsRegistry.running[registryKey]) test.Equal(t, 1, standaloneTestsRegistry.cleanup[registryKey]) }) t.Run("should pass tests with no diff", func(t *testing.T) { snapPath := setupSnapshot(t, standaloneFilename, false, "false") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { t.Error("should not be called 3rd time") }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } s := WithConfig(Update(true)) // First call for creating the snapshot s.MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchStandaloneSnapshot call standaloneTestsRegistry = newStandaloneRegistry() // Second call with same params s.MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, "hello world", test.GetFileContent(t, snapPath)) test.Equal(t, 1, testEvents.items[passed]) }) t.Run("if it's running on ci should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, standaloneFilename, true) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("if snaps.Update(false) should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, fileName, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } WithConfig(Update(false)).MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("should return error when diff is found", func(t *testing.T) { setupSnapshot(t, standaloneFilename, false) printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { t.Error("should not be called 2nd time") }, } mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { expected := "\n\x1b[38;5;52m\x1b[48;5;225m- Snapshot - 1\x1b[0m\n\x1b[38;5;22m\x1b[48;5;159m" + "+ Received + 1\x1b[0m\n\n\x1b[48;5;225m\x1b[38;5;52m- \x1b[0m\x1b[48;5;127m\x1b[38;5;255m" + "hello\x1b[0m\x1b[48;5;225m\x1b[38;5;52m world\x1b[0m\n\x1b[48;5;159m\x1b[38;5;22m" + "+ \x1b[0m\x1b[48;5;23m\x1b[38;5;255mbye\x1b[0m\x1b[48;5;159m\x1b[38;5;22m world\x1b[0m\n\n\x1b[2m" + "at " + filepath.FromSlash( "__snapshots__/mock-name_1.snap:1", ) + "\n\x1b[0m" test.Equal(t, expected, args[0].(string)) } mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchStandaloneSnapshot call standaloneTestsRegistry = newStandaloneRegistry() // Second call with different data MatchStandaloneSnapshot(mockT, "bye world") test.Equal(t, 1, testEvents.items[erred]) }) t.Run( "should create and update snapshot when UPDATE_SNAPS=always even on CI", func(t *testing.T) { snapPath := setupSnapshot(t, standaloneFilename, true, "always") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, func(received any) { t.Error("should not be called 3rd time") }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot WithConfig(Update(false)).MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchStandaloneSnapshot call standaloneTestsRegistry = newStandaloneRegistry() // Second call with different params WithConfig(Update(false)).MatchStandaloneSnapshot(mockT, "bye world") test.Equal(t, "bye world", test.GetFileContent(t, snapPath)) test.Equal(t, 1, testEvents.items[updated]) }, ) t.Run("should update snapshot", func(t *testing.T) { t.Run("when 'updateVAR==true'", func(t *testing.T) { snapPath := setupSnapshot(t, standaloneFilename, false, "true") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, func(received any) { t.Error("should not be called 3rd time") }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchStandaloneSnapshot call standaloneTestsRegistry = newStandaloneRegistry() // Second call with different params MatchStandaloneSnapshot(mockT, "bye world") test.Equal(t, "bye world", test.GetFileContent(t, snapPath)) test.Equal(t, 1, testEvents.items[updated]) }) t.Run("when config update", func(t *testing.T) { snapPath := setupSnapshot(t, standaloneFilename, false, "false") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, func(received any) { t.Error("should not be called 3rd time") }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } s := WithConfig(Update(true)) // First call for creating the snapshot s.MatchStandaloneSnapshot(mockT, "hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchStandaloneSnapshot call standaloneTestsRegistry = newStandaloneRegistry() // Second call with different params s.MatchStandaloneSnapshot(mockT, "bye world") test.Equal(t, "bye world", test.GetFileContent(t, snapPath)) test.Equal(t, 1, testEvents.items[updated]) }) }) } go-snaps-0.5.16/snaps/matchStandaloneYAML.go000066400000000000000000000076031511036776100206120ustar00rootroot00000000000000package snaps import ( "errors" "fmt" "strings" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/gkampitakis/go-snaps/match" ) /* MatchStandaloneYAML verifies the input matches the most recent snap file. Input can be a valid yaml string or []byte or whatever value can be passed successfully on `yaml.Marshal`. snaps.MatchStandaloneYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com") snaps.MatchStandaloneYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com")) snaps.MatchStandaloneYAML(t, User{10, "mock-email"}) MatchStandaloneYAML also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchStandaloneYAML(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("$.created")) MatchStandaloneYAML creates one snapshot file per call. You can call MatchStandaloneYAML multiple times inside a test. It will create multiple snapshot files at `__snapshots__` folder by default. */ func (c *Config) MatchStandaloneYAML(t testingT, input any, matchers ...match.YAMLMatcher) { t.Helper() if c.extension == "" { c.extension = ".yaml" } matchStandaloneYAML(c, t, input, matchers...) } /* MatchStandaloneYAML verifies the input matches the most recent snap file. Input can be a valid yaml string or []byte or whatever value can be passed successfully on `yaml.Marshal`. snaps.MatchStandaloneYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com") snaps.MatchStandaloneYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com")) snaps.MatchStandaloneYAML(t, User{10, "mock-email"}) MatchStandaloneYAML also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchStandaloneYAML(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("$.created")) MatchStandaloneYAML creates one snapshot file per call. You can call MatchStandaloneYAML multiple times inside a test. It will create multiple snapshot files at `__snapshots__` folder by default. */ func MatchStandaloneYAML(t testingT, input any, matchers ...match.YAMLMatcher) { t.Helper() c := defaultConfig if c.extension == "" { c.extension = ".yaml" } matchStandaloneYAML(&c, t, input, matchers...) } func matchStandaloneYAML(c *Config, t testingT, input any, matchers ...match.YAMLMatcher) { t.Helper() genericPathSnap, genericSnapPathRel := snapshotPath(c, t.Name(), true) snapPath, snapPathRel := standaloneTestsRegistry.getTestID(genericPathSnap, genericSnapPathRel) t.Cleanup(func() { standaloneTestsRegistry.reset(genericPathSnap) }) y, err := validateYAML(input) if err != nil { handleError(t, err) return } y, matchersErrors := applyYAMLMatchers(y, matchers...) if len(matchersErrors) > 0 { s := strings.Builder{} for _, err := range matchersErrors { colors.Fprint( &s, colors.Red, fmt.Sprintf( "\n%smatch.%s(\"%s\") - %s", errorSymbol, err.Matcher, err.Path, err.Reason, ), ) } handleError(t, s.String()) return } snapshot := takeYAMLSnapshot(y) prevSnapshot, err := getPrevStandaloneSnapshot(snapPath) if errors.Is(err, errSnapNotFound) { if !shouldCreate(c.update) { handleError(t, err) return } err := upsertStandaloneSnapshot(snapshot, snapPath) if err != nil { handleError(t, err) return } t.Log(addedMsg) testEvents.register(added) return } if err != nil { handleError(t, err) return } diff := prettyDiff( prevSnapshot, snapshot, snapPathRel, 1, ) if diff == "" { testEvents.register(passed) return } if !shouldUpdate(c.update) { handleError(t, diff) return } if err = upsertStandaloneSnapshot(snapshot, snapPath); err != nil { handleError(t, err) return } t.Log(updatedMsg) testEvents.register(updated) } go-snaps-0.5.16/snaps/matchStandaloneYAML_test.go000066400000000000000000000157331511036776100216540ustar00rootroot00000000000000package snaps import ( "errors" "fmt" "testing" "github.com/gkampitakis/go-snaps/internal/test" "github.com/gkampitakis/go-snaps/match" ) const yamlStandaloneFilename = "mock-name_1.snap.yaml" func TestMatchStandaloneYAML(t *testing.T) { t.Run("should create yaml snapshot", func(t *testing.T) { expected := `user: mock-name items: - 5 - 1 - 3 - 4 ` for _, tc := range []struct { name string input any }{ { name: "string", input: "user: mock-name\nitems:\n - 5\n - 1\n - 3\n - 4\n", }, { name: "byte", input: []byte("user: mock-name\nitems:\n - 5\n - 1\n - 3\n - 4\n"), }, { name: "marshal object", input: struct { User string `yaml:"user"` Items []int `yaml:"items"` }{ User: "mock-name", Items: []int{5, 1, 3, 4}, }, }, } { t.Run(tc.name, func(t *testing.T) { snapPath := setupSnapshot(t, yamlStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } MatchStandaloneYAML(mockT, tc.input) snap := test.GetFileContent(t, snapPath) test.Equal(t, expected, snap) test.Equal(t, 1, testEvents.items[added]) // clean up function called for _, v := range standaloneTestsRegistry.running { test.Equal(t, 0, v) } for _, v := range standaloneTestsRegistry.cleanup { test.Equal(t, 1, v) } }) } }) t.Run("should validate yaml", func(t *testing.T) { for _, tc := range []struct { name string input any err string }{ { name: "string", input: "key1: \"value1\nkey2: \"value2\"", err: `invalid yaml: [2:8] value is not allowed in this context. map key-value is pre-defined 1 | key1: "value1 > 2 | key2: "value2" ^ `, }, { name: "byte", input: []byte("key1: \"value1\nkey2: \"value2\""), err: `invalid yaml: [2:8] value is not allowed in this context. map key-value is pre-defined 1 | key1: "value1 > 2 | key2: "value2" ^ `, }, { name: "struct", input: make(chan struct{}), err: "invalid yaml: unknown value type chan struct {}", }, } { t.Run(tc.name, func(t *testing.T) { setupSnapshot(t, yamlStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, tc.err, (args[0].(error)).Error()) } MatchStandaloneYAML(mockT, tc.input) }) } }) t.Run("matchers", func(t *testing.T) { t.Run("should apply matches in order", func(t *testing.T) { setupSnapshot(t, yamlStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } mockT.MockError = func(a ...any) { fmt.Println(a...) } c1 := func(val any) (any, error) { return map[string]any{"key2": nil}, nil } c2 := func(val any) (any, error) { return map[string]any{"key3": nil}, nil } c3 := func(val any) (any, error) { return map[string]any{"key4": nil}, nil } MatchStandaloneYAML(mockT, "key1: \"\"", match.Custom("$.key1", c1), match.Custom("$.key1.key2", c2), match.Custom("$.key1.key2.key3", c3), ) }) t.Run("should aggregate errors from matchers", func(t *testing.T) { setupSnapshot(t, yamlStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, "\x1b[31;1m\nāœ• match.Custom(\"$.age\") - mock error"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"$.missing.key.1\") - path does not exist"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"$.missing.key.2\") - path does not exist\x1b[0m", args[0], ) } c := func(val any) (any, error) { return nil, errors.New("mock error") } MatchStandaloneYAML( mockT, `age: 10`, match.Custom("$.age", c), match.Any("$.missing.key.1", "$.missing.key.2"), ) }) }) t.Run("if it's running on ci should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, yamlStandaloneFilename, true) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } MatchStandaloneYAML(mockT, "") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("if snaps.Update(false) should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, yamlStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } WithConfig(Update(false)).MatchStandaloneYAML(mockT, "") test.Equal(t, 1, testEvents.items[erred]) }) t.Run( "should create and update snapshot when UPDATE_SNAPS=always even on CI", func(t *testing.T) { snapPath := setupSnapshot(t, yamlStandaloneFilename, true, "always") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot WithConfig(Update(false)).MatchStandaloneYAML(mockT, "value: hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params WithConfig(Update(false)).MatchStandaloneYAML(mockT, "value: bye world") test.Equal( t, "value: bye world", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }, ) t.Run("if snaps.Update(false) should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, yamlStandaloneFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } WithConfig(Update(false)).MatchStandaloneYAML(mockT, "") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("should update snapshot when 'shouldUpdate", func(t *testing.T) { snapPath := setupSnapshot(t, yamlStandaloneFilename, false, "true") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchStandaloneYAML(mockT, "value: hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params MatchStandaloneYAML(mockT, "value: bye world") test.Equal( t, "value: bye world", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }) } go-snaps-0.5.16/snaps/matchYAML.go000066400000000000000000000103351511036776100165750ustar00rootroot00000000000000package snaps import ( "errors" "fmt" "strings" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/gkampitakis/go-snaps/match" "github.com/goccy/go-yaml" ) var yamlEncodeOptions = []yaml.EncodeOption{ yaml.Indent(2), yaml.IndentSequence(true), } /* MatchYAML verifies the input matches the most recent snap file. Input can be a valid yaml string or []byte or whatever value can be passed successfully on `yaml.Marshal`. snaps.MatchYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com") snaps.MatchYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com")) snaps.MatchYAML(t, User{10, "mock-email"}) MatchYAML also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchYAML(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("$.created")) */ func (c *Config) MatchYAML(t testingT, input any, matchers ...match.YAMLMatcher) { t.Helper() matchYAML(c, t, input, matchers...) } /* MatchYAML verifies the input matches the most recent snap file. Input can be a valid yaml string or []byte or whatever value can be passed successfully on `yaml.Marshal`. snaps.MatchYAML(t, "user: \"mock-user\"\nage: 10\nemail: mock@email.com") snaps.MatchYAML(t, []byte("user: \"mock-user\"\nage: 10\nemail: mock@email.com")) snaps.MatchYAML(t, User{10, "mock-email"}) MatchYAML also supports passing matchers as a third argument. Those matchers can act either as validators or placeholders for data that might change on each invocation e.g. dates. snaps.MatchYAML(t, User{Created: time.Now(), Email: "mock-email"}, match.Any("$.created")) */ func MatchYAML(t testingT, input any, matchers ...match.YAMLMatcher) { t.Helper() matchYAML(&defaultConfig, t, input, matchers...) } func matchYAML(c *Config, t testingT, input any, matchers ...match.YAMLMatcher) { t.Helper() snapPath, snapPathRel := snapshotPath(c, t.Name(), false) testID := testsRegistry.getTestID(snapPath, t.Name()) t.Cleanup(func() { testsRegistry.reset(snapPath, t.Name()) }) y, err := validateYAML(input) if err != nil { handleError(t, err) return } y, matchersErrors := applyYAMLMatchers(y, matchers...) if len(matchersErrors) > 0 { s := strings.Builder{} for _, err := range matchersErrors { colors.Fprint( &s, colors.Red, fmt.Sprintf( "\n%smatch.%s(\"%s\") - %s", errorSymbol, err.Matcher, err.Path, err.Reason, ), ) } handleError(t, s.String()) return } snapshot := takeYAMLSnapshot(y) prevSnapshot, line, err := getPrevSnapshot(testID, snapPath) if errors.Is(err, errSnapNotFound) { if !shouldCreate(c.update) { handleError(t, err) return } err := addNewSnapshot(testID, snapshot, snapPath) if err != nil { handleError(t, err) return } t.Log(addedMsg) testEvents.register(added) return } if err != nil { handleError(t, err) return } diff := prettyDiff( unescapeEndChars(prevSnapshot), unescapeEndChars(snapshot), snapPathRel, line, ) if diff == "" { testEvents.register(passed) return } if !shouldUpdate(c.update) { handleError(t, diff) return } if err = updateSnapshot(testID, snapshot, snapPath); err != nil { handleError(t, err) return } t.Log(updatedMsg) testEvents.register(updated) } func validateYAML(input any) ([]byte, error) { var out any switch y := input.(type) { case string: err := yaml.Unmarshal([]byte(y), &out) if err != nil { return nil, fmt.Errorf("invalid yaml: %w", err) } return []byte(y), nil case []byte: err := yaml.Unmarshal(y, &out) if err != nil { return nil, fmt.Errorf("invalid yaml: %w", err) } return y, nil default: data, err := yaml.MarshalWithOptions(input, yamlEncodeOptions...) if err != nil { return nil, fmt.Errorf("invalid yaml: %w", err) } return data, nil } } func applyYAMLMatchers(b []byte, matchers ...match.YAMLMatcher) ([]byte, []match.MatcherError) { errors := []match.MatcherError{} for _, m := range matchers { y, errs := m.YAML(b) if len(errs) > 0 { errors = append(errors, errs...) continue } b = y } return b, errors } func takeYAMLSnapshot(b []byte) string { return escapeEndChars(string(b)) } go-snaps-0.5.16/snaps/matchYAML_test.go000066400000000000000000000152111511036776100176320ustar00rootroot00000000000000package snaps import ( "errors" "fmt" "testing" "github.com/gkampitakis/go-snaps/internal/test" "github.com/gkampitakis/go-snaps/match" ) const yamlFilename = "matchYAML_test.snap" func TestMatchYAML(t *testing.T) { t.Run("should create yaml snapshot", func(t *testing.T) { expected := `user: mock-name items: - 5 - 1 - 3 - 4 ` for _, tc := range []struct { name string input any }{ { name: "string", input: "user: mock-name\nitems:\n - 5\n - 1\n - 3\n - 4\n", }, { name: "byte", input: []byte("user: mock-name\nitems:\n - 5\n - 1\n - 3\n - 4\n"), }, { name: "marshal object", input: struct { User string `yaml:"user"` Items []int `yaml:"items"` }{ User: "mock-name", Items: []int{5, 1, 3, 4}, }, }, } { t.Run(tc.name, func(t *testing.T) { snapPath := setupSnapshot(t, yamlFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } MatchYAML(mockT, tc.input) snap, line, err := getPrevSnapshot("[mock-name - 1]", snapPath) test.NoError(t, err) test.Equal(t, 2, line) test.Equal(t, expected, snap) test.Equal(t, 1, testEvents.items[added]) // clean up function called test.Equal(t, 0, testsRegistry.running[snapPath]["mock-name"]) test.Equal(t, 1, testsRegistry.cleanup[snapPath]["mock-name"]) }) } t.Run("should validate yaml", func(t *testing.T) { for _, tc := range []struct { name string input any err string }{ { name: "string", input: "key1: \"value1\nkey2: \"value2\"", err: `invalid yaml: [2:8] value is not allowed in this context. map key-value is pre-defined 1 | key1: "value1 > 2 | key2: "value2" ^ `, }, { name: "byte", input: []byte("key1: \"value1\nkey2: \"value2\""), err: `invalid yaml: [2:8] value is not allowed in this context. map key-value is pre-defined 1 | key1: "value1 > 2 | key2: "value2" ^ `, }, { name: "struct", input: make(chan struct{}), err: "invalid yaml: unknown value type chan struct {}", }, } { t.Run(tc.name, func(t *testing.T) { setupSnapshot(t, yamlFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, tc.err, (args[0].(error)).Error()) } MatchYAML(mockT, tc.input) }) } }) t.Run("matchers", func(t *testing.T) { t.Run("should apply matches in order", func(t *testing.T) { setupSnapshot(t, yamlFilename, false) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, addedMsg, args[0].(string)) } mockT.MockError = func(a ...any) { fmt.Println(a...) } c1 := func(val any) (any, error) { return map[string]any{"key2": nil}, nil } c2 := func(val any) (any, error) { return map[string]any{"key3": nil}, nil } c3 := func(val any) (any, error) { return map[string]any{"key4": nil}, nil } MatchYAML(mockT, "key1: \"\"", match.Custom("$.key1", c1), match.Custom("$.key1.key2", c2), match.Custom("$.key1.key2.key3", c3), ) }) t.Run("should aggregate errors from matchers", func(t *testing.T) { setupSnapshot(t, yamlFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, "\x1b[31;1m\nāœ• match.Custom(\"$.age\") - mock error"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"$.missing.key.1\") - path does not exist"+ "\x1b[0m\x1b[31;1m\nāœ• match.Any(\"$.missing.key.2\") - path does not exist\x1b[0m", args[0], ) } c := func(val any) (any, error) { return nil, errors.New("mock error") } MatchYAML( mockT, `age: 10`, match.Custom("$.age", c), match.Any("$.missing.key.1", "$.missing.key.2"), ) }) }) t.Run("if it's running on ci should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, yamlFilename, true) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } MatchYAML(mockT, "") test.Equal(t, 1, testEvents.items[erred]) }) t.Run("if snaps.Update(false) should skip creating snapshot", func(t *testing.T) { setupSnapshot(t, yamlFilename, false) mockT := test.NewMockTestingT(t) mockT.MockError = func(args ...any) { test.Equal(t, errSnapNotFound, args[0].(error)) } WithConfig(Update(false)).MatchYAML(mockT, "") test.Equal(t, 1, testEvents.items[erred]) }) t.Run( "should create and update snapshot when UPDATE_SNAPS=always even on CI", func(t *testing.T) { snapPath := setupSnapshot(t, yamlFilename, true, "always") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot WithConfig(Update(false)).MatchYAML(mockT, "value: hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params WithConfig(Update(false)).MatchYAML(mockT, "value: bye world") test.Equal( t, "\n[mock-name - 1]\nvalue: bye world\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }, ) t.Run("should update snapshot when 'shouldUpdate'", func(t *testing.T) { snapPath := setupSnapshot(t, yamlFilename, false, "true") printerExpectedCalls := []func(received any){ func(received any) { test.Equal(t, addedMsg, received.(string)) }, func(received any) { test.Equal(t, updatedMsg, received.(string)) }, } mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { printerExpectedCalls[0](args[0]) // shift printerExpectedCalls = printerExpectedCalls[1:] } // First call for creating the snapshot MatchYAML(mockT, "value: hello world") test.Equal(t, 1, testEvents.items[added]) // Resetting registry to emulate the same MatchSnapshot call testsRegistry = newRegistry() // Second call with different params MatchYAML(mockT, "value: bye world") test.Equal( t, "\n[mock-name - 1]\nvalue: bye world\n---\n", test.GetFileContent(t, snapPath), ) test.Equal(t, 1, testEvents.items[updated]) }) }) } go-snaps-0.5.16/snaps/skip.go000066400000000000000000000041761511036776100157720ustar00rootroot00000000000000package snaps import ( "go/ast" "go/parser" "go/token" "path" "regexp" "strings" "github.com/gkampitakis/go-snaps/internal/colors" ) var ( skippedTests = newSyncSlice() skippedMsg = colors.Sprint(colors.Yellow, skipSymbol+"Snapshot skipped") ) // Wrapper of testing.Skip // // Keeps track which snapshots are getting skipped and not marked as obsolete. func Skip(t testingT, args ...any) { t.Helper() trackSkip(t) t.Skip(args...) } // Wrapper of testing.Skipf // // Keeps track which snapshots are getting skipped and not marked as obsolete. func Skipf(t testingT, format string, args ...any) { t.Helper() trackSkip(t) t.Skipf(format, args...) } // Wrapper of testing.SkipNow // // Keeps track which snapshots are getting skipped and not marked as obsolete. func SkipNow(t testingT) { t.Helper() trackSkip(t) t.SkipNow() } func trackSkip(t testingT) { t.Helper() t.Log(skippedMsg) skippedTests.append(t.Name()) } /* This checks if the parent test is skipped, or provided a 'runOnly' the testID is part of it e.g func TestParallel (t *testing.T) { snaps.Skip(t) ... } Then every "child" test should be skipped */ func testSkipped(testID, runOnly string) bool { // testID form: Test.*/runName - 1 testName := strings.Split(testID, " - ")[0] for _, name := range skippedTests.values { if testName == name || strings.HasPrefix(testName, name+"/") { return true } } matched, _ := regexp.MatchString(runOnly, testID) return !matched } func isFileSkipped(dir, filename, runOnly string) bool { // When a file is skipped through CLI with -run flag we can track it if runOnly == "" { return false } testFilePath := path.Join(dir, "..", strings.TrimSuffix(filename, snapsExt)+".go") fset := token.NewFileSet() file, err := parser.ParseFile(fset, testFilePath, nil, parser.ParseComments) if err != nil { return false } for _, decls := range file.Decls { funcDecl, ok := decls.(*ast.FuncDecl) if !ok { continue } // If the TestFunction is inside the file then it's not skipped matched, _ := regexp.MatchString(runOnly, funcDecl.Name.String()) if matched { return false } } return true } go-snaps-0.5.16/snaps/skip_test.go000066400000000000000000000116721511036776100170300ustar00rootroot00000000000000package snaps import ( "os" "sync" "sync/atomic" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestSkip(t *testing.T) { t.Run("should call Skip", func(t *testing.T) { t.Cleanup(func() { skippedTests = newSyncSlice() }) skipArgs := []any{1, 2, 3, 4, 5} mockT := test.NewMockTestingT(t) mockT.MockSkip = func(args ...any) { test.Equal(t, skipArgs, args) } mockT.MockLog = func(args ...any) { test.Equal(t, skippedMsg, args[0].(string)) } mockT.MockSkip = func(...any) {} Skip(mockT, 1, 2, 3, 4, 5) test.Equal(t, []string{"mock-name"}, skippedTests.values) }) t.Run("should call Skipf", func(t *testing.T) { t.Cleanup(func() { skippedTests = newSyncSlice() }) mockT := test.NewMockTestingT(t) mockT.MockSkipf = func(format string, args ...any) { test.Equal(t, "mock", format) test.Equal(t, []any{1, 2, 3, 4, 5}, args) } mockT.MockLog = func(args ...any) { test.Equal(t, skippedMsg, args[0].(string)) } mockT.MockSkipf = func(string, ...any) {} Skipf(mockT, "mock", 1, 2, 3, 4, 5) test.Equal(t, []string{"mock-name"}, skippedTests.values) }) t.Run("should call SkipNow", func(t *testing.T) { t.Cleanup(func() { skippedTests = newSyncSlice() }) mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, skippedMsg, args[0].(string)) } mockT.MockSkipNow = func() {} SkipNow(mockT) test.Equal(t, []string{"mock-name"}, skippedTests.values) }) t.Run("should be concurrent safe", func(t *testing.T) { t.Cleanup(func() { skippedTests = newSyncSlice() }) calledCount := atomic.Int64{} mockT := test.NewMockTestingT(t) mockT.MockLog = func(args ...any) { test.Equal(t, skippedMsg, args[0].(string)) } mockT.MockSkipNow = func() { calledCount.Add(1) } wg := sync.WaitGroup{} for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() SkipNow(mockT) }() } wg.Wait() test.Equal(t, 1000, len(skippedTests.values)) test.Equal(t, 1000, calledCount.Load()) }) t.Run("testSkipped", func(t *testing.T) { t.Run("should return true if testID is not part of the 'runOnly'", func(t *testing.T) { runOnly := "TestMock" testID := "TestSkip/should_call_Skip - 1" received := testSkipped(testID, runOnly) test.True(t, received) }) t.Run("should return false if testID is part of 'runOnly'", func(t *testing.T) { runOnly := "TestMock" testID := "TestMock/Test/should_be_not_skipped - 2" received := testSkipped(testID, runOnly) test.False(t, received) }) t.Run( "should check if the parent is skipped and mark child tests as skipped", func(t *testing.T) { t.Cleanup(func() { skippedTests = newSyncSlice() }) runOnly := "" mockT := test.NewMockTestingT(t) mockT.MockName = func() string { return "TestMock/Skip" } mockT.MockLog = func(args ...any) { test.Equal(t, skippedMsg, args[0].(string)) } mockT.MockSkipNow = func() {} // This is for populating skippedTests.values and following the normal flow SkipNow(mockT) test.True(t, testSkipped("TestMock/Skip - 1000", runOnly)) test.True( t, testSkipped("TestMock/Skip/child_should_also_be_skipped", runOnly), ) test.False(t, testSkipped("TestAnotherTest", runOnly)) }, ) t.Run("should not mark tests skipped if not not a child", func(t *testing.T) { t.Cleanup(func() { skippedTests = newSyncSlice() }) runOnly := "" mockT := test.NewMockTestingT(t) mockT.MockName = func() string { return "Test" } mockT.MockLog = func(args ...any) { test.Equal(t, skippedMsg, args[0].(string)) } mockT.MockSkipNow = func() {} // This is for populating skippedTests.values and following the normal flow SkipNow(mockT) test.True(t, testSkipped("Test - 1", runOnly)) test.True(t, testSkipped("Test/child - 1", runOnly)) test.False(t, testSkipped("TestMock - 1", runOnly)) test.False(t, testSkipped("TestMock/child - 1", runOnly)) }) t.Run("should use regex match for runOnly", func(t *testing.T) { test.False(t, testSkipped("MyTest - 1", "Test")) test.True(t, testSkipped("MyTest - 1", "^Test")) }) }) t.Run("isFileSkipped", func(t *testing.T) { t.Run("should return 'false'", func(t *testing.T) { test.False(t, isFileSkipped("", "", "")) }) t.Run("should return 'true' if test is not included in the test file", func(t *testing.T) { dir, _ := os.Getwd() test.Equal( t, true, isFileSkipped(dir+"/__snapshots__", "skip_test.snap", "TestNonExistent"), ) }) t.Run("should return 'false' if test is included in the test file", func(t *testing.T) { dir, _ := os.Getwd() test.False(t, isFileSkipped(dir+"/__snapshots__", "skip_test.snap", "TestSkip")) }) t.Run("should use regex match for runOnly", func(t *testing.T) { dir, _ := os.Getwd() test.Equal( t, false, isFileSkipped(dir+"/__snapshots__", "skip_test.snap", "TestSkip.*"), ) }) }) } go-snaps-0.5.16/snaps/snapshot.go000066400000000000000000000222121511036776100166520ustar00rootroot00000000000000package snaps import ( "bufio" "bytes" "fmt" "io" "os" "path/filepath" "strings" "sync" "github.com/gkampitakis/go-snaps/internal/colors" "github.com/tidwall/pretty" ) var ( testsRegistry = newRegistry() standaloneTestsRegistry = newStandaloneRegistry() _m = sync.RWMutex{} endSequenceByteSlice = []byte(endSequence) ) var ( addedMsg = colors.Sprint(colors.Green, updateSymbol+"Snapshot added") updatedMsg = colors.Sprint(colors.Green, updateSymbol+"Snapshot updated") ) type Config struct { filename string snapsDir string extension string update *bool json *JSONConfig } type JSONConfig struct { // Width is a max column width for single line arrays // Default: see defaultPrettyJSONOptions.Width for detail Width int // Indent is the nested indentation // Default: see defaultPrettyJSONOptions.Indent for detail Indent string // SortKeys will sort the keys alphabetically // Default: see defaultPrettyJSONOptions.SortKeys for detail SortKeys bool } func (j *JSONConfig) getPrettyJSONOptions() *pretty.Options { if j == nil { return defaultPrettyJSONOptions } return &pretty.Options{ Width: j.Width, Indent: j.Indent, SortKeys: j.SortKeys, } } // Update determines whether to update snapshots or not // // It respects if running on CI. func Update(u bool) func(*Config) { return func(c *Config) { c.update = &u } } // Specify snapshot file name // // default: test's filename // // this doesn't change the file extension see `snap.Ext` func Filename(name string) func(*Config) { return func(c *Config) { c.filename = name } } // Specify folder name where snapshots are stored // // default: __snapshots__ // // Accepts absolute paths func Dir(dir string) func(*Config) { return func(c *Config) { c.snapsDir = dir } } // Specify file name extension // // default: .snap // // Note: even if you specify a different extension the file still contain .snap // e.g. if you specify .txt the file will be .snap.txt func Ext(ext string) func(*Config) { return func(c *Config) { c.extension = ext } } // Specify json format configuration // // default: see defaultPrettyJSONOptions for default json config func JSON(json JSONConfig) func(*Config) { return func(c *Config) { c.json = &json } } // Create snaps with configuration // // e.g snaps.WithConfig(snaps.Filename("my_test")).MatchSnapshot(t, "hello world") func WithConfig(args ...func(*Config)) *Config { s := defaultConfig for _, arg := range args { arg(&s) } return &s } func handleError(t testingT, err any) { t.Helper() t.Error(err) testEvents.register(erred) } // We track occurrence as in the same test we can run multiple snapshots // This also helps with keeping track with obsolete snaps // map[snap path]: map[testname]: type syncRegistry struct { running map[string]map[string]int cleanup map[string]map[string]int sync.Mutex } // Returns the id of the test in the snapshot // Form [ - ] func (s *syncRegistry) getTestID(snapPath, testName string) string { s.Lock() if _, exists := s.running[snapPath]; !exists { s.running[snapPath] = make(map[string]int) s.cleanup[snapPath] = make(map[string]int) } s.running[snapPath][testName]++ s.cleanup[snapPath][testName]++ c := s.running[snapPath][testName] s.Unlock() return fmt.Sprintf("[%s - %d]", testName, c) } // reset sets only the number of running registry for the given test to 0. func (s *syncRegistry) reset(snapPath, testName string) { s.Lock() s.running[snapPath][testName] = 0 s.Unlock() } func newRegistry() *syncRegistry { return &syncRegistry{ running: make(map[string]map[string]int), cleanup: make(map[string]map[string]int), Mutex: sync.Mutex{}, } } type syncStandaloneRegistry struct { running map[string]int cleanup map[string]int sync.Mutex } func newStandaloneRegistry() *syncStandaloneRegistry { return &syncStandaloneRegistry{ running: make(map[string]int), cleanup: make(map[string]int), Mutex: sync.Mutex{}, } } func (s *syncStandaloneRegistry) getTestID(snapPath, snapPathRel string) (string, string) { s.Lock() s.running[snapPath]++ s.cleanup[snapPath]++ c := s.running[snapPath] s.Unlock() return fmt.Sprintf(snapPath, c), fmt.Sprintf(snapPathRel, c) } func (s *syncStandaloneRegistry) reset(snapPath string) { s.Lock() s.running[snapPath] = 0 s.Unlock() } // getPrevSnapshot scans file searching for a snapshot matching the given testID and returns // the snapshot with the line where is located inside the file. // // If not found returns errSnapNotFound error. func getPrevSnapshot(testID, snapPath string) (string, int, error) { _m.RLock() defer _m.RUnlock() f, err := os.ReadFile(snapPath) if err != nil { return "", -1, errSnapNotFound } lineNumber := 1 tid := []byte(testID) s := snapshotScanner(bytes.NewReader(f)) for s.Scan() { l := s.Bytes() if !bytes.Equal(l, tid) { lineNumber++ continue } var snapshot strings.Builder for s.Scan() { line := s.Bytes() if bytes.Equal(line, endSequenceByteSlice) { return strings.TrimSuffix(snapshot.String(), "\n"), lineNumber, nil } snapshot.Write(line) snapshot.WriteByte('\n') } } if err := s.Err(); err != nil { return "", -1, err } return "", -1, errSnapNotFound } func addNewSnapshot(testID, snapshot, snapPath string) error { if err := os.MkdirAll(filepath.Dir(snapPath), os.ModePerm); err != nil { return err } f, err := os.OpenFile(snapPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { return err } defer f.Close() _, err = fmt.Fprintf(f, "\n%s\n%s\n---\n", testID, snapshot) return err } func updateSnapshot(testID, snapshot, snapPath string) error { // When t.Parallel a test can override another snapshot as we dump // all snapshots _m.Lock() defer _m.Unlock() f, err := os.OpenFile(snapPath, os.O_RDWR, os.ModePerm) if err != nil { return err } defer f.Close() tid := []byte(testID) var updatedSnapFile bytes.Buffer i, err := f.Stat() if err == nil { updatedSnapFile.Grow(int(i.Size())) } s := snapshotScanner(f) for s.Scan() { b := s.Bytes() updatedSnapFile.Write(b) updatedSnapFile.WriteByte('\n') if !bytes.Equal(b, tid) { continue } removeSnapshot(s) // add new snapshot updatedSnapFile.WriteString(snapshot) updatedSnapFile.WriteByte('\n') updatedSnapFile.Write(endSequenceByteSlice) updatedSnapFile.WriteByte('\n') } if err := s.Err(); err != nil { return err } return overwriteFile(f, updatedSnapFile.Bytes()) } func overwriteFile(f *os.File, b []byte) error { f.Truncate(0) f.Seek(0, io.SeekStart) _, err := f.Write(b) return err } func removeSnapshot(s *bufio.Scanner) { for s.Scan() { // skip until --- if bytes.Equal(s.Bytes(), endSequenceByteSlice) { break } } } func upsertStandaloneSnapshot(snapshot, snapPath string) error { if err := os.MkdirAll(filepath.Dir(snapPath), os.ModePerm); err != nil { return err } return os.WriteFile(snapPath, []byte(snapshot), os.ModePerm) } func getPrevStandaloneSnapshot(snapPath string) (string, error) { f, err := os.ReadFile(snapPath) if err != nil { return "", errSnapNotFound } return string(f), nil } // Returns the path for snapshots: // - if no config provided returns the directory where tests are running // - if snapsDir is relative path just gets appended to directory where tests are running // - if snapsDir is absolute path then we are returning this path // // and for the filename: // - if no config provided we use the test file name with `.snap` extension // - if filename provided we return the filename with `.snap` extension // - if extension provided we return the filename with `.snap` and the provided extension // - if it's standalone snapshot we also append an integer (_%d) in the filename (even before `.snap`) // // Returns the relative path of the caller and the snapshot path. func snapshotPath(c *Config, tName string, isStandalone bool) (string, string) { // skips current func, the wrapper match* and the exported Match* func callerFilename, _ := baseCaller(3) dir := c.snapsDir if !filepath.IsAbs(dir) && !isTrimBathBuild { dir = filepath.Join(filepath.Dir(callerFilename), c.snapsDir) } snapPath := filepath.Join(dir, constructFilename(c, callerFilename, tName, isStandalone)) snapPathRel := snapPath if !isTrimBathBuild { snapPathRel, _ = filepath.Rel(filepath.Dir(callerFilename), snapPath) } return snapPath, snapPathRel } func constructFilename(c *Config, callerFilename, tName string, isStandalone bool) string { filename := c.filename if filename == "" { base := filepath.Base(callerFilename) filename = strings.TrimSuffix(base, filepath.Ext(base)) if isStandalone { filename = strings.ReplaceAll(tName, "/", "_") } } if isStandalone { filename += "_%d" } filename += snapsExt + c.extension return filename } func unescapeEndChars(s string) string { ss := strings.Split(s, "\n") for idx, s := range ss { if s == "/-/-/-/" { ss[idx] = endSequence } } return strings.Join(ss, "\n") } func escapeEndChars(s string) string { ss := strings.Split(s, "\n") for idx, s := range ss { if s == endSequence { ss[idx] = "/-/-/-/" } } return strings.Join(ss, "\n") } go-snaps-0.5.16/snaps/snapshot_test.go000066400000000000000000000250231511036776100177140ustar00rootroot00000000000000package snaps import ( "path/filepath" "sync" "testing" "github.com/gkampitakis/go-snaps/internal/test" ) func TestSyncRegistry(t *testing.T) { t.Run("should increment id on each call [concurrent safe]", func(t *testing.T) { wg := sync.WaitGroup{} registry := newRegistry() for i := 0; i < 5; i++ { wg.Add(1) go func() { registry.getTestID("/file", "test") wg.Done() }() } wg.Wait() test.Equal(t, "[test - 6]", registry.getTestID("/file", "test")) test.Equal(t, "[test-v2 - 1]", registry.getTestID("/file", "test-v2")) test.Equal(t, registry.cleanup, registry.running) }) t.Run("should reset running registry", func(t *testing.T) { wg := sync.WaitGroup{} registry := newRegistry() for i := 0; i < 100; i++ { wg.Add(1) go func() { registry.getTestID("/file", "test") wg.Done() }() } wg.Wait() registry.reset("/file", "test") // running registry start from 0 again test.Equal(t, "[test - 1]", registry.getTestID("/file", "test")) // cleanup registry still has 101 test.Equal(t, 101, registry.cleanup["/file"]["test"]) }) } func TestSyncStandaloneRegistry(t *testing.T) { t.Run("should increment id on each call [concurrent safe]", func(t *testing.T) { wg := sync.WaitGroup{} registry := newStandaloneRegistry() for i := 0; i < 5; i++ { wg.Add(1) go func() { registry.getTestID("/file/my_file_%d.snap", "./__snapshots__/my_file_%d.snap") wg.Done() }() } wg.Wait() snapPath, snapPathRel := registry.getTestID( "/file/my_file_%d.snap", "./__snapshots__/my_file_%d.snap", ) test.Equal(t, "/file/my_file_6.snap", snapPath) test.Equal(t, "./__snapshots__/my_file_6.snap", snapPathRel) snapPath, snapPathRel = registry.getTestID( "/file/my_other_file_%d.snap", "./__snapshots__/my_other_file_%d.snap", ) test.Equal(t, "/file/my_other_file_1.snap", snapPath) test.Equal(t, "./__snapshots__/my_other_file_1.snap", snapPathRel) test.Equal(t, registry.cleanup, registry.running) }) t.Run("should reset running registry", func(t *testing.T) { wg := sync.WaitGroup{} registry := newStandaloneRegistry() for i := 0; i < 100; i++ { wg.Add(1) go func() { registry.getTestID("/file/my_file_%d.snap", "./__snapshots__/my_file_%d.snap") wg.Done() }() } wg.Wait() registry.reset("/file/my_file_%d.snap") snapPath, snapPathRel := registry.getTestID( "/file/my_file_%d.snap", "./__snapshots__/my_file_%d.snap", ) // running registry start from 0 again test.Equal(t, "/file/my_file_1.snap", snapPath) test.Equal(t, "./__snapshots__/my_file_1.snap", snapPathRel) // cleanup registry still has 101 test.Equal(t, 101, registry.cleanup["/file/my_file_%d.snap"]) }) } func TestGetPrevSnapshot(t *testing.T) { t.Run("should return errSnapNotFound", func(t *testing.T) { snap, line, err := getPrevSnapshot("", "") test.Equal(t, "", snap) test.Equal(t, -1, line) test.Equal(t, errSnapNotFound, err) }) t.Run("should return errSnapNotFound if no match found", func(t *testing.T) { fileData := "[testid]\ntest\n---\n" path := test.CreateTempFile(t, fileData) snap, line, err := getPrevSnapshot("nonexistentid", path) test.Equal(t, "", snap) test.Equal(t, -1, line) test.Equal(t, errSnapNotFound, err) }) for _, scenario := range []struct { description string testID string fileData string snap string line int err error }{ { description: "should not match if no data", testID: "my-test", fileData: "", snap: "", line: -1, err: errSnapNotFound, }, { description: "should not match", testID: "my-test", fileData: "mysnapshot", snap: "", line: -1, err: errSnapNotFound, }, { description: "should return match", testID: "[my-test - 1]", fileData: "[my-test - 1]\nmysnapshot\n---\n", snap: "mysnapshot", line: 1, }, { description: "should ignore regex in testID and match correct snap", testID: "[.*]", fileData: "\n[my-test]\nwrong snap\n---\n\n[.*]\nmysnapshot\n---\n", snap: "mysnapshot", line: 6, }, { description: "should ignore end chars (---) inside snapshot", testID: "[mock-test 1]", fileData: "\n[mock-test 1]\nmysnapshot\n---moredata\n---\n", snap: "mysnapshot\n---moredata", line: 2, }, { description: "should keep terminal \r character inside snapshot data", testID: "[my-test-crlf]", fileData: "\n[one-more-snap]\nmock-data\r\n---\n[my-test-crlf]\nline1\r\nline2\r\nline3\r\n---\n", snap: "line1\r\nline2\r\nline3\r", line: 5, }, } { s := scenario t.Run(s.description, func(t *testing.T) { t.Parallel() path := test.CreateTempFile(t, s.fileData) snap, line, err := getPrevSnapshot(s.testID, path) test.Equal(t, s.err, err) test.Equal(t, s.line, line) test.Equal(t, s.snap, snap) }) } } func TestAddNewSnapshot(t *testing.T) { snapPath := filepath.Join(t.TempDir(), "__snapshots__/mock-test.snap") test.NoError(t, addNewSnapshot("[mock-id]", "my-snap", snapPath)) test.Equal(t, "\n[mock-id]\nmy-snap\n---\n", test.GetFileContent(t, snapPath)) } func TestSnapshotPath(t *testing.T) { snapshotPathWrapper := func(c *Config, tName string, isStandalone bool) (snapPath, snapPathRel string) { // This is for emulating being called from a func so we can find the correct file // of the caller func() { func() { snapPath, snapPathRel = snapshotPath(c, tName, isStandalone) }() }() return snapPath, snapPathRel } t.Run("should return default path and file", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&defaultConfig, "", false) test.HasSuffix(t, snapPath, filepath.FromSlash("/snaps/__snapshots__/snapshot_test.snap")) test.Equal(t, filepath.FromSlash("__snapshots__/snapshot_test.snap"), snapPathRel) }) t.Run("should return path and file from config", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&Config{ filename: "my_file", snapsDir: "my_snapshot_dir", }, "", false) // returns the current file's path /snaps/* test.HasSuffix(t, snapPath, filepath.FromSlash("/snaps/my_snapshot_dir/my_file.snap")) test.Equal(t, filepath.FromSlash("my_snapshot_dir/my_file.snap"), snapPathRel) }) t.Run("should return absolute path", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&Config{ filename: "my_file", snapsDir: "/path_to/my_snapshot_dir", }, "", false) test.HasSuffix(t, snapPath, filepath.FromSlash("/path_to/my_snapshot_dir/my_file.snap")) // the depth depends on filesystem structure test.HasSuffix( t, snapPathRel, filepath.FromSlash("path_to/my_snapshot_dir/my_file.snap"), ) }) t.Run("should add extension to filename", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&Config{ filename: "my_file", snapsDir: "my_snapshot_dir", extension: ".txt", }, "", false) test.HasSuffix(t, snapPath, filepath.FromSlash("/snaps/my_snapshot_dir/my_file.snap.txt")) test.Equal(t, filepath.FromSlash("my_snapshot_dir/my_file.snap.txt"), snapPathRel) }) t.Run("should return standalone snapPath", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&defaultConfig, "my_test", true) test.HasSuffix( t, snapPath, filepath.FromSlash("/snaps/__snapshots__/my_test_%d.snap"), ) test.Equal( t, filepath.FromSlash("__snapshots__/my_test_%d.snap"), snapPathRel, ) }) t.Run("should return standalone snapPath without '/'", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&defaultConfig, "TestFunction/my_test", true) test.HasSuffix( t, snapPath, filepath.FromSlash("/snaps/__snapshots__/TestFunction_my_test_%d.snap"), ) test.Equal( t, filepath.FromSlash("__snapshots__/TestFunction_my_test_%d.snap"), snapPathRel, ) }) t.Run("should return standalone snapPath with overridden filename", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&Config{ filename: "my_file", snapsDir: "my_snapshot_dir", }, "my_test", true) test.HasSuffix(t, snapPath, filepath.FromSlash("/snaps/my_snapshot_dir/my_file_%d.snap")) test.Equal(t, filepath.FromSlash("my_snapshot_dir/my_file_%d.snap"), snapPathRel) }) t.Run( "should return standalone snapPath with overridden filename and extension", func(t *testing.T) { snapPath, snapPathRel := snapshotPathWrapper(&Config{ filename: "my_file", snapsDir: "my_snapshot_dir", extension: ".txt", }, "my_test", true) test.HasSuffix( t, snapPath, filepath.FromSlash("/snaps/my_snapshot_dir/my_file_%d.snap.txt"), ) test.Equal(t, filepath.FromSlash("my_snapshot_dir/my_file_%d.snap.txt"), snapPathRel) }, ) } func TestUpdateSnapshot(t *testing.T) { t.Run("should update snapshot", func(t *testing.T) { const updatedSnap = ` [Test_1/TestSimple - 1] int(1) string hello world 1 1 1 --- [Test_3/TestSimple - 1] int(1250) string new value --- [Test_3/TestSimple - 2] int(1000) string hello world 1 3 2 --- ` snapPath := test.CreateTempFile(t, mockSnap) newSnapshot := "int(1250)\nstring new value" test.NoError(t, updateSnapshot("[Test_3/TestSimple - 1]", newSnapshot, snapPath)) test.Equal(t, updatedSnap, test.GetFileContent(t, snapPath)) }) t.Run("should not drop terminal \r from snapshot data", func(t *testing.T) { snapPath := test.CreateTempFile( t, "\n[mock-id]\nline1\r\nline2\r\nline3\r\n---\n[another-id]\nmoredata\n---\n", ) newSnapshot := "updated-line1\r\nupdated-line2\r\nupdated-line3\r" test.NoError(t, updateSnapshot("[mock-id]", newSnapshot, snapPath)) test.Equal( t, "\n[mock-id]\nupdated-line1\r\nupdated-line2\r\nupdated-line3\r\n---\n[another-id]\nmoredata\n---\n", test.GetFileContent(t, snapPath), ) }) } func TestEscapeEndChars(t *testing.T) { t.Run("should escape end chars inside data", func(t *testing.T) { snapPath := filepath.Join(t.TempDir(), "__snapshots__/mock-test.snap") snapshot := takeSnapshot([]any{"my-snap", endSequence}) test.NoError(t, addNewSnapshot("[mock-id]", snapshot, snapPath)) test.Equal(t, "\n[mock-id]\nmy-snap\n/-/-/-/\n---\n", test.GetFileContent(t, snapPath)) }) t.Run("should not escape --- if not end chars", func(t *testing.T) { snapPath := filepath.Join(t.TempDir(), "__snapshots__/mock-test.snap") snapshot := takeSnapshot([]any{"my-snap---", endSequence}) test.NoError(t, addNewSnapshot("[mock-id]", snapshot, snapPath)) test.Equal(t, "\n[mock-id]\nmy-snap---\n/-/-/-/\n---\n", test.GetFileContent(t, snapPath)) }) } go-snaps-0.5.16/snaps/testdata/000077500000000000000000000000001511036776100162765ustar00rootroot00000000000000go-snaps-0.5.16/snaps/testdata/mock-snap-1000066400000000000000000000004141511036776100202460ustar00rootroot00000000000000[TestDir1_3/TestSimple - 1] int(100) string hello world 1 3 1 --- [TestDir1_2/TestSimple - 1] int(10) string hello world 1 2 1 --- [TestDir1_3/TestSimple - 2] int(1000) string hello world 1 3 2 --- [TestDir1_1/TestSimple - 1] int(1) string hello world 1 1 1 --- go-snaps-0.5.16/snaps/testdata/mock-snap-2000066400000000000000000000004121511036776100202450ustar00rootroot00000000000000 [TestDir2_2/TestSimple - 1] int(1000) string hello world 2 2 1 --- [TestDir2_1/TestSimple - 1] int(1) string hello world 2 1 1 --- [TestDir2_1/TestSimple - 3] int(100) string hello world 2 1 3 --- [TestDir2_1/TestSimple - 2] int(10) string hello world 2 1 2 --- go-snaps-0.5.16/snaps/testdata/mock-snap-sort-1000066400000000000000000000006661511036776100212440ustar00rootroot00000000000000 [TestDir1_1/TestSimple - 1] int(1) string hello world 1 1 1 --- [TestCat - 1] string("") --- [TestAlpha - 2] mock snapshot second value --- [TestDir1_2/TestSimple - 1] int(10) string hello world 1 2 1 --- [TestBeta - 1] mock snapshot and another value --- [TestDir1_3/TestSimple - 1] int(100) string hello world 1 3 1 --- [TestDir1_3/TestSimple - 2] int(1000) string hello world 1 3 2 --- [TestAlpha - 1] mock snapshot --- go-snaps-0.5.16/snaps/testdata/mock-snap-sort-1-sorted000066400000000000000000000006661511036776100225420ustar00rootroot00000000000000 [TestAlpha - 1] mock snapshot --- [TestAlpha - 2] mock snapshot second value --- [TestBeta - 1] mock snapshot and another value --- [TestCat - 1] string("") --- [TestDir1_1/TestSimple - 1] int(1) string hello world 1 1 1 --- [TestDir1_2/TestSimple - 1] int(10) string hello world 1 2 1 --- [TestDir1_3/TestSimple - 1] int(100) string hello world 1 3 1 --- [TestDir1_3/TestSimple - 2] int(1000) string hello world 1 3 2 --- go-snaps-0.5.16/snaps/testdata/mock-snap-sort-2000066400000000000000000000006631511036776100212420ustar00rootroot00000000000000 [TestAlpha - 1] mock snapshot --- [TestBeta - 1] mock snapshot and another value --- [TestDir2_1/TestSimple - 1] int(1) string hello world 2 1 1 --- [TestDir2_2/TestSimple - 1] int(1000) string hello world 2 2 1 --- [TestDir2_1/TestSimple - 2] int(10) string hello world 2 1 2 --- [TestDir2_1/TestSimple - 3] int(100) string hello world 2 1 3 --- [TestCat - 1] string("") --- [TestAlpha - 2] mock snapshot second value --- go-snaps-0.5.16/snaps/testdata/mock-snap-sort-2-sorted000066400000000000000000000006631511036776100225400ustar00rootroot00000000000000 [TestAlpha - 1] mock snapshot --- [TestAlpha - 2] mock snapshot second value --- [TestBeta - 1] mock snapshot and another value --- [TestCat - 1] string("") --- [TestDir2_1/TestSimple - 1] int(1) string hello world 2 1 1 --- [TestDir2_1/TestSimple - 2] int(10) string hello world 2 1 2 --- [TestDir2_1/TestSimple - 3] int(100) string hello world 2 1 3 --- [TestDir2_2/TestSimple - 1] int(1000) string hello world 2 2 1 --- go-snaps-0.5.16/snaps/utils.go000066400000000000000000000070261511036776100161610ustar00rootroot00000000000000package snaps import ( "bufio" "bytes" "errors" "io" "math" "os" "path/filepath" "runtime" "runtime/debug" "slices" "strings" "sync" "github.com/gkampitakis/ciinfo" ) var ( errSnapNotFound = errors.New("snapshot not found") isCI = ciinfo.IsCI updateVAR = os.Getenv("UPDATE_SNAPS") shouldClean = updateVAR == "always" || (updateVAR == "true" && !isCI) || (updateVAR == "clean" && !isCI) defaultConfig = Config{ snapsDir: "__snapshots__", } isTrimBathBuild = trimPathBuild() ) const ( arrowSymbol = "› " bulletSymbol = "• " errorSymbol = "āœ• " successSymbol = "āœ“ " updateSymbol = "āœŽ " skipSymbol = "⟳ " enterSymbol = "↳ " newLineSymbol = "↵" snapsExt = ".snap" endSequence = "---" ) type ( set map[string]struct{} testingT interface { Helper() Skip(...any) Skipf(string, ...any) SkipNow() Name() string Error(...any) Log(...any) Cleanup(func()) } ) func (s set) Has(i string) bool { _, has := s[i] return has } type syncSlice struct { values []string sync.Mutex } func (s *syncSlice) append(elems ...string) { s.Lock() defer s.Unlock() s.values = append(s.values, elems...) } func newSyncSlice() *syncSlice { return &syncSlice{ values: []string{}, Mutex: sync.Mutex{}, } } // Returns the path where the "user" tests are running func baseCaller(skip int) (string, int) { var ( pc uintptr file, prevFile string line, prevLine int ok bool ) for i := skip + 1; ; i++ { prevLine = line prevFile = file pc, file, line, ok = runtime.Caller(i) if !ok { return prevFile, prevLine } f := runtime.FuncForPC(pc) if f == nil { return prevFile, prevLine } if f.Name() == "testing.tRunner" { return prevFile, prevLine } if strings.HasSuffix(filepath.Base(file), "_test.go") { return file, line } } } // snapshotScanner returns a new *bufio.Scanner with a `MaxScanTokenSize == math.MaxInt` to read from r. func snapshotScanner(r io.Reader) *bufio.Scanner { s := bufio.NewScanner(r) s.Split(scanLines) s.Buffer([]byte{}, math.MaxInt) return s } // shouldUpdate determines whether snapshots should be updated func shouldUpdate(u *bool) bool { if updateVAR == "always" { return true } if isCI { return false } if u != nil { return *u } return updateVAR == "true" } // code taken from bufio/scan.go, modified to not terminal \r from the data. func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { return 0, nil, nil } if i := bytes.IndexByte(data, '\n'); i >= 0 { // We have a full newline-terminated line. return i + 1, data[0:i], nil } // If we're at EOF, we have a final, non-terminated line. Return it. if atEOF { return len(data), data, nil } // Request more data. return 0, nil, nil } // shouldCreate determines whether snapshots should be created func shouldCreate(u *bool) bool { if updateVAR == "always" { return true } if isCI { return false } if u != nil { return *u } return true } // trimPathBuild checks if the build has trimpath setting true func trimPathBuild() bool { keys := []string{"-trimpath", "--trimpath"} goFlags := strings.Split(os.Getenv("GOFLAGS"), " ") for _, flag := range goFlags { if slices.Contains(keys, flag) { return true } } bInfo, ok := debug.ReadBuildInfo() if ok && len(bInfo.Settings) > 0 { for _, info := range bInfo.Settings { if slices.Contains(keys, info.Key) { return info.Value == "true" } } } return runtime.GOROOT() == "" } go-snaps-0.5.16/snaps/utils_test.go000066400000000000000000000025351511036776100172200ustar00rootroot00000000000000package snaps import ( "strings" "testing" ) func TestScanLines_CRLF(t *testing.T) { tests := []struct { name string input string expected []string }{ { name: "LF only", input: "line1\nline2\nline3\n", expected: []string{"line1", "line2", "line3"}, }, { name: "CRLF lines", input: "line1\r\nline2\r\nline3\r\n", expected: []string{"line1\r", "line2\r", "line3\r"}, }, { name: "Mixed endings", input: "line1\r\nline2\nline3\r\nline4\n", expected: []string{"line1\r", "line2", "line3\r", "line4"}, }, { name: "No final newline", input: "line1\r\nline2", expected: []string{"line1\r", "line2"}, }, { name: "Empty input", input: "", expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { scanner := snapshotScanner(strings.NewReader(tt.input)) var lines []string for scanner.Scan() { lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { t.Fatalf("unexpected scan error: %v", err) } if len(lines) != len(tt.expected) { t.Fatalf("expected %d lines, got %d: %v", len(tt.expected), len(lines), lines) } for i := range lines { if lines[i] != tt.expected[i] { t.Errorf("line %d: expected %q, got %q", i, tt.expected[i], lines[i]) } } }) } }