Repository: uber-go/fx Branch: master Commit: d5da5b04ac90 Files: 191 Total size: 770.9 KB Directory structure: gitextract_i617t6c0/ ├── .codecov.yml ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── docs.yml │ ├── fossa.yaml │ └── go.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── annotated.go ├── annotated_test.go ├── app.go ├── app_internal_test.go ├── app_test.go ├── app_unixes.go ├── app_wasm.go ├── app_windows.go ├── app_windows_test.go ├── broadcast.go ├── decorate.go ├── decorate_test.go ├── doc.go ├── docs/ │ ├── .gitattributes │ ├── .gitignore │ ├── Makefile │ ├── ex/ │ │ ├── annotate/ │ │ │ ├── cast.go │ │ │ ├── cast_bad.go │ │ │ ├── cast_test.go │ │ │ ├── github/ │ │ │ │ └── stub.go │ │ │ ├── sample.go │ │ │ └── sample_test.go │ │ ├── get-started/ │ │ │ ├── 01-minimal/ │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── 02-http-server/ │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── 03-echo-handler/ │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── 04-logger/ │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── 05-registration/ │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ ├── 06-another-handler/ │ │ │ │ ├── main.go │ │ │ │ └── main_test.go │ │ │ └── 07-many-handlers/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ ├── modules/ │ │ │ ├── module.go │ │ │ └── module_test.go │ │ ├── parameter-objects/ │ │ │ ├── define.go │ │ │ ├── define_test.go │ │ │ ├── extend.go │ │ │ └── extend_test.go │ │ ├── result-objects/ │ │ │ ├── define.go │ │ │ ├── define_test.go │ │ │ ├── extend.go │ │ │ └── extend_test.go │ │ └── value-groups/ │ │ ├── consume/ │ │ │ ├── annotate.go │ │ │ ├── consume_test.go │ │ │ └── param.go │ │ └── feed/ │ │ ├── annotate.go │ │ ├── feed_test.go │ │ └── result.go │ ├── go.mod │ ├── go.sum │ ├── internal/ │ │ ├── apptest/ │ │ │ ├── run.go │ │ │ └── run_test.go │ │ ├── exectest/ │ │ │ ├── cmd.go │ │ │ ├── cmd_test.go │ │ │ ├── output.go │ │ │ └── output_test.go │ │ ├── httptest/ │ │ │ ├── http.go │ │ │ └── http_test.go │ │ ├── iotest/ │ │ │ ├── read.go │ │ │ └── read_test.go │ │ └── test/ │ │ ├── fake.go │ │ ├── fake_test.go │ │ ├── t.go │ │ └── t_test.go │ ├── mkdocs.yml │ ├── pyproject.toml │ └── src/ │ ├── annotate.md │ ├── container.md │ ├── faq.md │ ├── get-started/ │ │ ├── another-handler.md │ │ ├── conclusion.md │ │ ├── echo-handler.md │ │ ├── http-server.md │ │ ├── index.md │ │ ├── logger.md │ │ ├── many-handlers.md │ │ ├── minimal.md │ │ └── registration.md │ ├── index.md │ ├── lifecycle.md │ ├── modules.md │ ├── parameter-objects.md │ ├── result-objects.md │ └── value-groups/ │ ├── consume.md │ ├── feed.md │ └── index.md ├── error_example_test.go ├── example_test.go ├── extract.go ├── extract_test.go ├── fxevent/ │ ├── console.go │ ├── console_test.go │ ├── doc.go │ ├── event.go │ ├── event_test.go │ ├── logger.go │ ├── slog.go │ ├── slog_test.go │ ├── zap.go │ └── zap_test.go ├── fxtest/ │ ├── app.go │ ├── app_test.go │ ├── doc.go │ ├── lifecycle.go │ ├── lifecycle_test.go │ ├── printer.go │ ├── printer_test.go │ ├── tb.go │ └── tb_test.go ├── go.mod ├── go.sum ├── inout.go ├── inout_test.go ├── internal/ │ ├── e2e/ │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ ├── shutdowner_run_exitcode/ │ │ │ ├── main.go │ │ │ └── main_test.go │ │ └── shutdowner_wait_exitcode/ │ │ ├── main.go │ │ └── main_test.go │ ├── fxclock/ │ │ ├── clock.go │ │ └── clock_test.go │ ├── fxlog/ │ │ ├── default.go │ │ ├── default_test.go │ │ ├── foovendor/ │ │ │ └── foovendor.go │ │ ├── sample.git/ │ │ │ └── sample.go │ │ ├── spy.go │ │ └── spy_test.go │ ├── fxreflect/ │ │ ├── fxreflect.go │ │ ├── fxreflect_test.go │ │ ├── stack.go │ │ └── stack_test.go │ ├── leaky_test/ │ │ └── leaky_test.go │ ├── lifecycle/ │ │ ├── lifecycle.go │ │ └── lifecycle_test.go │ └── testutil/ │ ├── writer.go │ └── writer_test.go ├── invoke.go ├── lifecycle.go ├── log.go ├── log_test.go ├── module.go ├── module_test.go ├── populate.go ├── populate_example_test.go ├── populate_test.go ├── printer_writer.go ├── provide.go ├── replace.go ├── replace_test.go ├── shutdown.go ├── shutdown_test.go ├── signal.go ├── signal_test.go ├── supply.go ├── supply_test.go ├── tools/ │ ├── analysis/ │ │ └── passes/ │ │ └── allfxevents/ │ │ ├── analysis.go │ │ ├── analysis_test.go │ │ └── testdata/ │ │ └── src/ │ │ ├── a/ │ │ │ ├── full.go │ │ │ ├── nop.go │ │ │ ├── not_a_logger.go │ │ │ ├── partial_test.go │ │ │ ├── ptr.go │ │ │ └── value.go │ │ ├── b/ │ │ │ ├── fxevent/ │ │ │ │ └── logger.go │ │ │ └── not_real_fxevent.go │ │ └── go.uber.org/ │ │ └── fx/ │ │ └── fxevent/ │ │ ├── fxevent.go │ │ └── partial.go │ ├── cmd/ │ │ └── fxlint/ │ │ └── main.go │ ├── go.mod │ └── go.sum └── version.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ ignore: - "docs/ex/**/*.go" - "internal/e2e/**/*.go" coverage: range: 80..100 round: down precision: 2 status: project: # measuring the overall project coverage default: # context, you can create multiple ones with custom titles enabled: yes # must be yes|true to enable this status target: 90% # specify the target coverage for each commit status # option: "auto" (must increase from parent commit or pull request base) # option: "X%" a static target percentage to hit if_not_found: success # if parent is not found report status as success, error, or failure if_ci_failed: error # if ci fails report status as success, error, or failure ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior **Expected behavior** A clear and concise description of what you expected to happen. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Questions about: Please use our Discussions page url: https://github.com/uber-go/fx/discussions ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Is this a breaking change?** We do not accept breaking changes to the existing API. Please consider if your proposed solution is backwards compatible. If not, we can help you make it backwards compatible, but this must be considered when we consider new features. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" # Auto-update tools dependencies, but not library dependencies. - package-ecosystem: "gomod" directory: "/tools" schedule: interval: "weekly" ================================================ FILE: .github/workflows/docs.yml ================================================ name: GitHub Pages on: push: tags: ['v*'] # This lets us publish the workflow # manually from the GitHub Actions UI. # # It expects a single input: # the Git ref we want to build and publish the docs for. workflow_dispatch: inputs: head: description: "Git commitish to check out." required: true type: string # Run at most one publish job at a time, # cancelling others if a new one starts. concurrency: group: "pages" cancel-in-progress: true env: UV_VERSION: 0.3.3 jobs: build: runs-on: ubuntu-latest steps: # We have two checkout steps running based on # whether the workflow was manually triggered. # If manually triggered, we use the provided ref, # otherwise we use the default ref. - name: Checkout (on release) if: github.event_name != 'workflow_dispatch' uses: actions/checkout@v4 - name: Checkout (manual) if: github.event_name == 'workflow_dispatch' uses: actions/checkout@v4 with: ref: ${{ inputs.head }} - name: Install uv run: | curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh - name: Build run: make docs - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: docs/_site deploy: needs: build # run only after a successful build permissions: pages: write # to deploy to Pages id-token: write # to verify the deployment originates from an appropriate source environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/fossa.yaml ================================================ name: FOSSA Analysis on: push jobs: build: runs-on: ubuntu-latest if: github.repository_owner == 'uber-go' steps: - name: Checkout code uses: actions/checkout@v4 - name: FOSSA analysis uses: fossas/fossa-action@v1 with: api-key: ${{ secrets.FOSSA_API_KEY }} permissions: contents: read ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: branches: ['*'] tags: ['v*'] pull_request: branches: ['*'] permissions: contents: read jobs: build: runs-on: ${{ matrix.os }} name: Build and test strategy: matrix: os: ["ubuntu-latest", "windows-latest"] go: ["1.24.x", "1.25.x"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Download Dependencies run: go mod download - name: Test run: make cover - name: Upload coverage to codecov.io uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} doctest: runs-on: ubuntu-latest name: Test documentation steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.25.x - name: Test run: make cover COVER_MODULES=./docs lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.25.x - uses: golangci/golangci-lint-action@v8 name: Install golangci-lint with: version: latest args: --help # make lint will run the linter - run: make lint name: Lint ================================================ FILE: .gitignore ================================================ /vendor /.bench *.mem *.cpu *.test *.log *.out *.html *.coverprofile coverage.txt *.pprof /.bin /.cache /bin .vscode .mdoxcache ================================================ FILE: .golangci.yml ================================================ version: "2" issues: # Print all issues reported by all linters. max-issues-per-linter: 0 max-same-issues: 0 linters: # We'll track the golangci-lint default linters manually # instead of letting them change without our control. default: none enable: # golangci-lint defaults: - govet - ineffassign - staticcheck - unused # Our own extras: - errorlint - nolintlint # lints //nolint directives - revive # License header check: - goheader settings: # These govet checks are disabled by default, but they're useful. govet: enable: - nilness - reflectvaluecompare - sortslice - unusedwrite goheader: values: const: COMPANY: Uber Technologies, Inc. regexp: YEAR_RANGE: \d{4}(-\d{4})? template: |- Copyright (c) {{ YEAR_RANGE }} {{ COMPANY }} 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. exclusions: generated: lax rules: # Don't warn on unused parameters. # Parameter names are useful; replacing them with '_' is undesirable. - linters: [revive] text: 'unused-parameter: parameter \S+ seems to be unused, consider removing or renaming it as _' # staticcheck already has smarter checks for empty blocks. # revive's empty-block linter has false positives. # For example, as of writing this, the following is not allowed. # for foo() { } - linters: [revive] text: 'empty-block: this block is empty, you can remove it' # It's okay if internal packages and examples in docs/ # don't have package comments. - linters: [revive] path: .+/internal/.+|^internal/.+|^docs/.+ text: should have a package comment # It's okay for tests to use dot imports. - linters: [revive] path: _test\.go$ text: should not use dot imports formatters: enable: [gofumpt] exclusions: generated: lax ================================================ FILE: CHANGELOG.md ================================================ --- search: exclude: true --- # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - No changes yet. ## [1.24.0](https://github.com/uber-go/fx/compare/v1.23.0...v1.24.0) - 2025-05-13 ### Added - A new event `fxevent.BeforeRun` is now emitted before Fx runs a constructor, decorator, or supply/replace stub. ### Changed - Clearer error messages are now used when annotation building fails. ## [1.23.0](https://github.com/uber-go/fx/compare/v1.22.2...v1.22.3) - 2024-10-11 ### Added - Added `Runtime` to `fxevent.Run` event, which stores the runtime of a constructor or a decorator that's run, including functions created by `fx.Supply` and `fx.Replace`. ### Changed - Overhauled the documentation website. (https://uber-go.github.io/fx/) ## [1.22.2](https://github.com/uber-go/fx/compare/v1.22.1...v1.22.2) - 2024-08-07 ### Fixed - A deadlock with the relayer in signal receivers. ### Changed - Upgrade Dig dependency to v1.18.0 ## [1.22.1](https://github.com/uber-go/fx/compare/v1.22.0...v1.22.1) - 2024-06-25 ### Fixed - Fx apps will only listen to signals when `.Run()`, `.Wait()`, or `.Done()` are called, fixing a regression introduced in v1.19.0. ## [1.22.0](https://github.com/uber-go/fx/compare/v1.21.1...v1.22.0) - 2024-05-30 ### Added - Add `fx.Self` which can be passed to the `fx.As` annotation to signify that a type should be provided as itself. - Add `fxtest.EnforceTimeout` that can be passed to `fxtest.NewLifecycle` to force `Start` and `Stop` to return context errors when hook context expires. ### Changed - `fx.Private` can now be used with `fx.Supply`. ### Fixed - Fx apps will no longer listen to OS signals when they are stopped, solving blocking issues in programs that depended on OS signals after an Fx app stops. ## [1.21.1](https://github.com/uber-go/fx/compare/v1.21.0...v1.21.1) - 2024-04-24 ### Changed - Register Fx provides (e.g. fx.Lifecycle, fx.Shutdowner, fx.DotGraph) before user provides, to increase likelihood of successful custom logger creation. ## [1.21.0](https://github.com/uber-go/fx/compare/v1.20.1...v1.21.0) - 2024-03-13 ### Added - fxtest: Add WithTestLogger option that uses a `testing.TB` as the Fx event logger. - An fxevent logger that can log events using a slog logger has been added. ### Changed - Upgrade Dig dependency to v1.17.1 ## [1.20.1](https://github.com/uber-go/fx/compare/v1.20.0...v1.20.1) - 2023-10-17 ### Added - Provided, Decorated, Supplied, and Replaced events now include a trace of module locations through which the option was given to the App. - wasi support. ## [1.20.0](https://github.com/uber-go/fx/compare/v1.19.3...v1.20.0) - 2023-06-12 ### Added - A new event `fxevent.Run` is now emitted when Fx runs a constructor, decorator, or supply/replace stub. ### Changed - `fx.Populate` now works with `fx.Annotate`. - Upgrade Dig dependency to v1.17.0. ## [1.19.3](https://github.com/uber-go/fx/compare/v1.19.2...v1.19.3) - 2023-04-17 ### Changed - Fixed several typos in docs. - WASM build support. - Annotating In and Out structs with From/As annotations generated invalid results. The annotation check now blocks this. - `Shutdown`: Support calling from `Invoke`. ### Deprecated - Deprecate `ShutdownTimeout` option. ### Fixed - Respect Shutdowner ExitCode from calling `Run`. ## [1.19.2](https://github.com/uber-go/fx/compare/v1.19.1...v1.19.2) - 2023-02-21 ### Changed - Update Dig dependency to v1.16.1. ## [1.19.1](https://github.com/uber-go/fx/compare/v1.19.0...v1.19.1) - 2023-01-10 ### Changed - Calling `fx.Stop()` after the `App` has already stopped no longer errors out. ### Fixed - Addressed a regression in 1.19.0 release which caused apps to ignore OS signals after running for startTimeout duration. ## [1.19.0](https://github.com/uber-go/fx/compare/v1.18.2...v1.19.0) - 2023-01-03 ### Added - `fx.RecoverFromPanics` Option which allows Fx to recover from user-provided constructors and invoked functions. - `fx.Private` that allows the constructor to limit the scope of its outputs to the wrapping `fx.Module`. - `ExitCode` ShutdownOption which allows setting custom exit code at the end of app lifecycle. - `Wait` which returns a channel that can be used for waiting on application shutdown. - fxevent/ZapLogger now exposes `UseLogLevel` and `UseErrorLevel` methods to set the level of the Zap logs produced by it. - Add lifecycle hook-convertible methods: `StartHook`, `StopHook`, `StartStopHook` that can be used with more function signatures. ### Changed - `fx.WithLogger` can now be passed at `fx.Module` level, setting custom logger at `Module` scope instead of the whole `App`. ### Fixed - `fx.OnStart` and `fx.OnStop` Annotations now work with annotated types that was provided by the annotated constructor. - fxevent/ZapLogger: Errors from `fx.Supply` are now logged at `Error` level, not `Info`. - A race condition in lifecycle Start/Stop methods. - Typos in docs. ## [1.18.2](https://github.com/uber-go/fx/compare/v1.18.1...v1.18.2) - 2022-09-28 ### Added - Clarify ordering of `Invoke`s in `Module`s. ### Fixed - Fix `Decorate` not being applied to transitive dependencies at root `App` level. ## [1.18.1](https://github.com/uber-go/fx/compare/v1.18.0...v1.18.1) - 2022-08-08 ### Fixed - Fix a nil panic when `nil` is passed to `OnStart` and `OnStop` lifecycle methods. ## [1.18.0](https://github.com/uber-go/fx/compare/v1.17.1...v1.18.0) - 2022-08-05 ### Added - Soft value groups that lets you specify value groups as best-effort dependencies. - `fx.OnStart` and `fx.OnStop` annotations which lets you annotate dependencies to provide OnStart and OnStop lifecycle hooks. - A new `fxevent.Replaced` event written to `fxevent.Logger` following an `fx.Replace`. ### Fixed - Upgrade Dig dependency to v1.14.1 to address a couple of issues with decorations. Refer to Dig v1.14.1 release notes for more details. - `fx.WithLogger` no longer ignores decorations and replacements of types that it depends on. - Don't run lifecycle hooks if the context for them has already expired. - `App.Start` and `App.Stop` no longer deadlock if the OnStart/OnStop hook exits the current goroutine. - `fxevent.ConsoleLogger` no longer emits an extraneous argument for the Supplied event. ### Deprecated - `fx.Extract` in favor of `fx.Populate`. ## [1.17.1](https://github.com/uber-go/fx/compare/v1.17.0...v1.17.1) - 2022-03-23 ### Added - Logging for provide/invoke/decorate now includes the associated `fx.Module` name. ## [1.17.0](https://github.com/uber-go/fx/compare/v1.16.0...v1.17.0) - 2022-02-28 ### Added - Add `fx.Module` which scopes any modifications made to the dependency graph. - Add `fx.Decorate` and `fx.Replace` that lets you modify a dependency graph with decorators. - Add `fxevent.Decorated` event which gets emitted upon a dependency getting decorated. ### Changed - `fx.Annotate`: Validate that `fx.In` or `fx.Out` structs are not passed to it. - `fx.Annotate`: Upon failure to Provide, the error contains the actual location of the provided constructor. ## [1.16.0](https://github.com/uber-go/fx/compare/v1.15.0...v1.16.0) - 2021-12-02 ### Added - Add the ability to provide a function as multiple interfaces at once using `fx.As`. ### Changed - `fx.Annotate`: support variadic functions, and feeding value groups into them. ### Fixed - Fix an issue where OnStop hooks weren't getting called on SIGINT on Windows. - Fix a data race between app.Done() and shutdown. ## [1.15.0](https://github.com/uber-go/fx/compare/v1.14.2...v1.15.0) - 2021-11-08 ### Added - Add `fx.Annotate` to allow users to provide parameter and result tags easily without having to create `fx.In` or `fx.Out` structs. - Add `fx.As` that allows users to annotate a constructor to provide its result type(s) as interface(s) that they implement instead of the types themselves. ### Fixed - Fix `fxevent.Stopped` not being logged when `App.Stop` is called. - Fix `fxevent.Started` or `fxevent.Stopped` not being logged when start or stop times out. ## [1.14.2](https://github.com/uber-go/fx/compare/v1.14.1...v1.14.2) - 2021-08-16 ### Changed - For `fxevent` console implementation: no longer log non-error case for `fxevent.Invoke` event, while for zap implementation, start logging `fx.Invoking` case without stack. ## [1.14.1](https://github.com/uber-go/fx/compare/v1.14.0...v1.14.1) - 2021-08-16 ### Changed - `fxevent.Invoked` was being logged at `Error` level even upon successful `Invoke`. This was changed to log at `Info` level when `Invoke` succeeded. ## [1.14.0](https://github.com/uber-go/fx/compare/v1.13.1...v1.14.0) - 2021-08-12 ### Added - Introduce the new `fx.WithLogger` option. Provide a constructor for `fxevent.Logger` objects with it to customize how Fx logs events. - Add new `fxevent` package that exposes events from Fx in a structured way. Use this to write custom logger implementations for use with the `fx.WithLogger` option. - Expose and log additional information when lifecycle hooks time out. ### Changed - Fx now emits structured JSON logs by default. These may be parsed and processed by log ingestion systems. - `fxtest.Lifecycle` now logs to the provided `testing.TB` instead of stderr. - `fx.In` and `fx.Out` are now type aliases instead of structs. ## [1.13.1](https://github.com/uber-go/fx/compare/v1.13.0...v1.13.1) - 2020-08-19 ### Fixed - Fix minimum version constraint for dig. `fx.ValidateGraph` requires at least dig 1.10. ## [1.13.0](https://github.com/uber-go/fx/compare/v1.12.0...v1.13.0) - 2020-06-16 ### Added - Added `fx.ValidateGraph` which allows graph cycle validation and dependency correctness without running anything. This is useful if `fx.Invoke` has side effects, does I/O, etc. ## [1.12.0](https://github.com/uber-go/fx/compare/v1.11.0...v1.12.0) - 2020-04-09 ### Added - Added `fx.Supply` to provide externally created values to Fx containers without building anonymous constructors. ### Changed - Drop library dependency on development tools. ## [1.11.0](https://github.com/uber-go/fx/compare/v1.10.0...v1.11.0) - 2020-04-01 ### Added - Value groups can use the `flatten` option to indicate values in a slice should be provided individually rather than providing the slice itself. See package documentation for details. ## [1.10.0](https://github.com/uber-go/fx/compare/v1.9.0...v1.10.0) - 2019-11-20 ### Added - All `fx.Option`s now include readable string representations. - Report stack traces when `fx.Provide` and `fx.Invoke` calls fail. This should make these errors more debuggable. ### Changed - Migrated to Go modules. ## [1.9.0](https://github.com/uber-go/fx/compare/v1.8.0...v1.9.0) - 2019-01-22 ### Added - Add the ability to shutdown Fx applications from inside the container. See the Shutdowner documentation for details. - Add `fx.Annotated` to allow users to provide named values without creating a new constructor. ## [1.8.0](https://github.com/uber-go/fx/compare/v1.7.1...v1.8.0) - 2018-11-06 ### Added - Provide DOT graph of dependencies in the container. ## [1.7.1](https://github.com/uber-go/fx/compare/v1.7.0...v1.7.1) - 2018-09-26 ### Fixed - Make `fxtest.New` ensure that the app was created successfully. Previously, it would return the app (similar to `fx.New`, which expects the user to verify the error). - Update dig container to defer acyclic validation until after Invoke. Application startup time should improve proportional to the size of the dependency graph. - Fix a goroutine leak in `fxtest.Lifecycle`. ## [1.7.0](https://github.com/uber-go/fx/compare/v1.6.0...v1.7.0) - 2018-08-16 ### Added - Add `fx.ErrorHook` option to allow users to provide `ErrorHandler`s on invoke failures. - `VisualizeError` returns the visualization wrapped in the error if available. ## [1.6.0](https://github.com/uber-go/fx/compare/v1.5.0...v1.6.0) - 2018-06-12 ### Added - Add `fx.Error` option to short-circuit application startup. ## [1.5.0](https://github.com/uber-go/fx/compare/v1.4.0...v1.5.0) - 2018-04-11 ### Added - Add `fx.StartTimeout` and `fx.StopTimeout` to make configuring application start and stop timeouts easier. - Export the default start and stop timeout as `fx.DefaultTimeout`. ### Fixed - Make `fxtest` respect the application's start and stop timeouts. ## [1.4.0](https://github.com/uber-go/fx/compare/v1.3.0...v1.4.0) - 2017-12-07 ### Added - Add `fx.Populate` to populate variables with values from the dependency injection container without requiring intermediate structs. ## [1.3.0](https://github.com/uber-go/fx/compare/v1.2.0...v1.3.0) - 2017-11-28 ### Changed - Improve readability of hook logging in addition to provide and invoke. ### Fixed - Fix bug which caused the OnStop for a lifecycle hook to be called even if it failed to start. ## [1.2.0](https://github.com/uber-go/fx/compare/v1.1.0...v1.2.0) - 2017-09-06 ### Added - Add `fx.NopLogger` which disables the Fx application's log output. ## [1.1.0](https://github.com/uber-go/fx/compare/v1.0.0...v1.1.0) - 2017-08-22 ### Changed - Improve readability of start up logging. ## [1.0.0](https://github.com/uber-go/fx/compare/v1.0.0-rc2...v1.0.0) - 2017-07-31 First stable release: no breaking changes will be made in the 1.x series. ### Added - `fx.Extract` now supports `fx.In` tags on target structs. ### Changed - **[Breaking]** Rename `fx.Inject` to `fx.Extract`. - **[Breaking]** Rename `fxtest.Must*` to `fxtest.Require*`. ### Removed - **[Breaking]** Remove `fx.Timeout` and `fx.DefaultTimeout`. ## [1.0.0-rc2](https://github.com/uber-go/fx/compare/v1.0.0-rc1...v1.0.0-rc2) - 2017-07-21 - **[Breaking]** Lifecycle hooks now take a context. - Add `fx.In` and `fx.Out` which exposes optional and named types. Modules should embed these types instead of relying on `dig.In` and `dig.Out`. - Add an `Err` method to retrieve the underlying errors during the dependency graph construction. The same error is also returned from `Start`. - Graph resolution now happens as part of `fx.New`, rather than at the beginning of `app.Start`. This allows inspection of the graph errors through `app.Err()` before the decision to start the app. - Add a `Logger` option, which allows users to send Fx's logs to different sink. - Add `fxtest.App`, which redirects log output to the user's `testing.TB` and provides some lifecycle helpers. ## [1.0.0-rc1](https://github.com/uber-go/fx/compare/v1.0.0-beta4...v1.0.0-rc1) - 2017-06-20 - **[Breaking]** Providing types into `fx.App` and invoking functions are now options passed during application construction. This makes users' interactions with modules and collections of modules identical. - **[Breaking]** `TestLifecycle` is now in a separate `fxtest` subpackage. - Add `fx.Inject()` to pull values from the container into a struct. ## [1.0.0-beta4](https://github.com/uber-go/fx/compare/v1.0.0-beta3...v1.0.0-beta4) - 2017-06-12 - **[Breaking]** Monolithic framework, as released in initial betas, has been broken into smaller pieces as a result of recent advances in `dig` library. This is a radical departure from the previous direction, but it needed to be done for the long-term good of the project. - **[Breaking]** `Module interface` has been scoped all the way down to being *a single dig constructor*. This allows for very sophisticated module compositions. See `go.uber.org/dig` for more information on the constructors. - **[Breaking]** `package config` has been moved to its own repository. see `go.uber.org/config` for more information. - `fx.Lifecycle` has been added for modules to hook into the framework lifecycle events. - `service.Host` interface which composed a number of primitives together (configuration, metrics, tracing) has been deprecated in favor of `fx.App`. ## [1.0.0-beta3](https://github.com/uber-go/fx/compare/v1.0.0-beta2...v1.0.0-beta3) - 2017-03-28 - **[Breaking]** Environment config provider was removed. If you were using environment variables to override YAML values, see config documentation for more information. - **[Breaking]** Simplify Provider interface: remove `Scope` method from the `config.Provider` interface, one can use either ScopedProvider and Value.Get() to access sub fields. - Add `task.MustRegister` convenience function which fails fast by panicking Note that this should only be used during app initialization, and is provided to avoid repetetive error checking for services which register many tasks. - Expose options on task module to disable execution. This will allow users to enqueue and consume tasks on different clusters. - **[Breaking]** Rename Backend interface `Publish` to `Enqueue`. Created a new `ExecuteAsync` method that will kick off workers to consume tasks and this is subsumed by module Start. - **[Breaking]** Rename package `uhttp/client` to `uhttp/uhttpclient` for clarity. - **[Breaking]** Rename `PopulateStruct` method in value to `Populate`. The method can now populate not only structs, but anything: slices, maps, builtin types and maps. - **[Breaking]** `package dig` has moved from `go.uber.org/fx/dig` to a new home at `go.uber.org/dig`. - **[Breaking]** Pass a tracer the `uhttp/uhttpclient` constructor explicitly, instead of using a global tracer. This will allow to use http client in parallel tests. ## [1.0.0-beta2](https://github.com/uber-go/fx/compare/v1.0.0-beta1...v1.0.0-beta2) - 2017-03-09 - **[Breaking]** Remove `ulog.Logger` interface and expose `*zap.Logger` directly. - **[Breaking]** Rename config and module from `modules.rpc` to `modules.yarpc` - **[Breaking]** Rename config key from `modules.http` to `modules.uhttp` to match the module name - **[Breaking]** Upgrade `zap` to `v1.0.0-rc.3` (now go.uber.org/zap, was github.com/uber-go/zap) - Remove now-unused `config.IsDevelopmentEnv()` helper to encourage better testing practices. Not a breaking change as nobody is using this func themselves according to our code search tool. - Log `traceID` and `spanID` in hex format to match Jaeger UI. Upgrade Jaeger to min version 2.1.0 and use jaeger's adapters for jaeger and tally initialization. - Tally now supports reporting histogram samples for a bucket. Upgrade Tally to 2.1.0 - **[Breaking]** Make new module naming consistent `yarpc.ThriftModule` to `yarpc.New`, `task.NewModule` to `task.New` - **[Breaking]** Rename `yarpc.CreateThriftServiceFunc` to `yarpc.ServiceCreateFunc` as it is not thrift-specific. - Report version metrics for company-wide version usage information. - Allow configurable service name and module name via service options. - DIG constructors now support returning a tuple with the second argument being an error. ## 1.0.0-beta1 - 2017-02-20 This is the first beta release of the framework, where we invite users to start building services on it and provide us feedback. **Warning** we are not promising API compatibility between beta releases and the final 1.0.0 release. In fact, we expect our beta user feedback to require some changes to the way things work. Once we reach 1.0, we will provider proper version compatibility. ================================================ FILE: CONTRIBUTING.md ================================================ --- search: exclude: true --- # Contributing Thanks for helping to make Fx better for everyone! If you'd like to add new exported APIs, please [open an issue](https://github.com/uber-go/fx/issues/new) describing your proposal. Discussing API changes ahead of time makes pull request review much smoother. !!! tip You'll need to sign [Uber's CLA](https://cla-assistant.io/uber-go/fx) before we can accept any of your contributions. If necessary, a bot will remind you to accept the CLA when you open your pull request. ## Contribute code Set up your local development environment to contribute to Fx. 1. [Fork](https://github.com/uber-go/fx/fork), then clone the repository. === "Git" ```bash git clone https://github.com/your_github_username/fx.git cd fx git remote add upstream https://github.com/uber-go/fx.git git fetch upstream ``` === "GitHub CLI" ```bash gh repo fork --clone uber-go/fx ``` 2. Install Fx's dependencies: ```bash go mod download ``` 3. Verify that tests and other checks pass locally. ```bash make lint make test ``` Note that for `make lint` to work, you must be using the latest stable version of Go. If you're on an older version, you can still contribute your change, but we may discover style violations when you open the pull request. Next, make your changes. 1. Create a new feature branch. ```bash git checkout master git pull git checkout -b cool_new_feature ``` 2. Make your changes, and verify that all tests and lints still pass. ```bash $EDITOR app.go make lint make test ``` 3. When you're satisfied with the change, push it to your fork and make a pull request. === "Git" ```bash git push origin cool_new_feature # Open a PR at https://github.com/uber-go/fx/compare ``` === "GitHub CLI" ```bash gh pr create ``` At this point, you're waiting on us to review your changes. We *try* to respond to issues and pull requests within a few business days, and we may suggest some improvements or alternatives. Once your changes are approved, one of the project maintainers will merge them. The review process will go more smoothly if you: - add tests for new functionality - write a [good commit message](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) - maintain backward compatibility - follow our [style guide](https://github.com/uber-go/guide/blob/master/style.md) ## Contribute documentation To contribute documentation to Fx, 1. Set up your local development environment as you would to [contribute code](#contribute-code). 2. [Install uv](https://docs.astral.sh/uv/getting-started/installation/). We use this to manage our Python dependencies. 3. Run the development server. ```bash make serve ``` 4. Make your changes. Documentation changes should adhere to the guidance laid out below. ### Document by purpose Documentation is organized in one of the following categories. - **Tutorials**: These hold step-by-step instructions for an end-to-end project that a beginner could follow along to. Don't spend time explaining things. If explanations are available elsewhere, link to them. These are entry points to answer the prompt, "I don't know what Fx is, show me what it can do," so there won't be too many of these. - **Explanations**: These hold long-form explanations of concepts and ideas. These are intended to build an understanding of Fx. Feel free to go wild here--use learning aids like diagrams, tables, etc. - **How-tos**: These are step-by-step instructions for a *specific problem*. Unlike tutorials, these are not meant to be end-to-end. Feel free to leave things out, make assumptions, or provide options ("if you're doing this, do this"). As with tutorials, don't spend time explaining; link to explanations elsewhere. As an example, - A tutorial will use lifecycle hooks as part of a larger set of instructions for a full end-to-end application. - An explanation will explain what lifecycle hooks are, how they work, when and how you should use them, and link to relevant APIs and guides. - A how-to guide will demonstrate how to use lifecycle hooks with an HTTP server, a gRPC server, etc. Explanations and how-to guides are often on the same page, but they should be in distinct sections. This separation is inspired by the [Divio documentation system](https://documentation.divio.com/), ### Formatting #### ATX-style headers Use ATX-style headers (`#`-prefixed), not Setext-style (underlined with `===` or `---`). ```markdown Bad header ========== ## Good header ``` #### Semantic Line Breaks - **Do not** write overly long lines of text - **Do not** "reflow" Markdown paragraphs - **Do** use [Semantic Line Breaks](https://sembr.org/) to break these lines down ```markdown This is a bad paragraph because it's really long, all on one line. When I open this in a text editor, I'll have to scroll right. This is a bad paragraph because even though it's not all one one line, it adds line breaks when it reaches the line length limit. This means that anytime I change anything in this paragraph, I have to "reflow" it, which will change other lines and make the change I'm making more difficult to review. This is a good paragraph. It uses semantic line breaks. I can add words or modify an existing sentence, or even parts of a sentence, easily and without affecting other lines. When I change something, the actual change I made is easy to review. Markdown will reflow this into a "normal" pargraph when rendering. ``` ### Test everything All code samples in documentation must be buildable and testable. To make this possible, we put code samples in the "ex/" directory, and use the [PyMdown Snippets extension](https://facelessuser.github.io/pymdown-extensions/extensions/snippets/) to include them in the documentation. To include code snippets in your documentation, take the following steps: 1. Add source code under the `ex/` directory. Usually, the file will be placed in a directory with a name matching the documentation file that will include the snippet. For example, for src/annotation.md, examples will reside in ex/annotation/. 2. Inside the source file, name regions of code with comments in the forms: ``` // --8<-- [start:name] ... // --8<-- [end:name] ``` Where `name` is the name of the snippet. For example: ```go // --8<-- [start:New] func New() *http.Server { // ... } // --8<-- [end:New] ``` 3. Include the snippet in a code block with the following syntax: ```markdown ```go ;--8<-- "path/to/file.go:name" ``` Where `path/to/file.go` is the path to the file containing the snippet relative to the `ex/` directory, and `name` is the name of the snippet. You can include multiple snippets from the same file like so: ``` ;--8<-- "path/to/file.go:snippet1" ;--8<-- "path/to/file.go:snippet2" ``` ================================================ FILE: LICENSE ================================================ Copyright (c) 2016-2018 Uber Technologies, Inc. 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. ================================================ FILE: Makefile ================================================ # Directory containing the Makefile. PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) export GOBIN ?= $(PROJECT_ROOT)/bin export PATH := $(GOBIN):$(PATH) FXLINT = $(GOBIN)/fxlint MODULES = . ./tools ./docs ./internal/e2e # 'make cover' should not run on docs by default. # We run that separately explicitly on a specific platform. COVER_MODULES ?= $(filter-out ./docs,$(MODULES)) .PHONY: all all: build lint test .PHONY: build build: go build ./... .PHONY: lint lint: golangci-lint tidy-lint fx-lint .PHONY: test test: @$(foreach dir,$(MODULES),(cd $(dir) && go test -race ./...) &&) true .PHONY: cover cover: @$(foreach dir,$(COVER_MODULES), \ (cd $(dir) && \ echo "[cover] $(dir)" && \ go test -race -coverprofile=cover.out -coverpkg=./... ./... && \ go tool cover -html=cover.out -o cover.html) &&) true .PHONY: tidy tidy: @$(foreach dir,$(MODULES),(cd $(dir) && go mod tidy) &&) true .PHONY: docs docs: cd docs && make build .PHONY: golangci-lint golangci-lint: @$(foreach mod,$(MODULES), \ (cd $(mod) && \ echo "[lint] golangci-lint: $(mod)" && \ golangci-lint run --path-prefix $(mod)) &&) true .PHONY: tidy-lint tidy-lint: @$(foreach mod,$(MODULES), \ (cd $(mod) && \ echo "[lint] tidy: $(mod)" && \ go mod tidy -diff) &&) true .PHONY: fx-lint fx-lint: $(FXLINT) @$(FXLINT) ./... $(FXLINT): tools/cmd/fxlint/main.go cd tools && go install go.uber.org/fx/tools/cmd/fxlint ================================================ FILE: README.md ================================================ # :unicorn: Fx [![GoDoc](https://pkg.go.dev/badge/go.uber.org/fx)](https://pkg.go.dev/go.uber.org/fx) [![Github release](https://img.shields.io/github/release/uber-go/fx.svg)](https://github.com/uber-go/fx/releases) [![Build Status](https://github.com/uber-go/fx/actions/workflows/go.yml/badge.svg)](https://github.com/uber-go/fx/actions/workflows/go.yml) [![Coverage Status](https://codecov.io/gh/uber-go/fx/branch/master/graph/badge.svg)](https://codecov.io/gh/uber-go/fx/branch/master) [![Go Report Card](https://goreportcard.com/badge/go.uber.org/fx)](https://goreportcard.com/report/go.uber.org/fx) Fx is a dependency injection system for Go. **Benefits** - Eliminate globals: Fx helps you remove global state from your application. No more `init()` or global variables. Use Fx-managed singletons. - Code reuse: Fx lets teams within your organization build loosely-coupled and well-integrated shareable components. - Battle tested: Fx is the backbone of nearly all Go services at Uber. See our [docs](https://uber-go.github.io/fx/) to get started and/or learn more about Fx. ## Installation Use Go modules to install Fx in your application. ```shell go get go.uber.org/fx@v1 ``` ## Getting started To get started with Fx, [start here](https://uber-go.github.io/fx/get-started/). ## Stability This library is `v1` and follows [SemVer](https://semver.org/) strictly. No breaking changes will be made to exported APIs before `v2.0.0`. This project follows the [Go Release Policy](https://golang.org/doc/devel/release.html#policy). Each major version of Go is supported until there are two newer major releases. ## Stargazers over time [![Stargazers over time](https://starchart.cc/uber-go/fx.svg)](https://starchart.cc/uber-go/fx) ================================================ FILE: annotated.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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. package fx import ( "context" "errors" "fmt" "reflect" "strings" "go.uber.org/dig" "go.uber.org/fx/internal/fxreflect" ) // Annotated annotates a constructor provided to Fx with additional options. // // For example, // // func NewReadOnlyConnection(...) (*Connection, error) // // fx.Provide(fx.Annotated{ // Name: "ro", // Target: NewReadOnlyConnection, // }) // // Is equivalent to, // // type result struct { // fx.Out // // Connection *Connection `name:"ro"` // } // // fx.Provide(func(...) (result, error) { // conn, err := NewReadOnlyConnection(...) // return result{Connection: conn}, err // }) // // Annotated cannot be used with constructors which produce fx.Out objects. // When used with [Supply], Target is a value instead of a constructor. // // This type represents a less powerful version of the [Annotate] construct; // prefer [Annotate] where possible. type Annotated struct { // If specified, this will be used as the name for all non-error values returned // by the constructor. For more information on named values, see the documentation // for the fx.Out type. // // A name option may not be provided if a group option is provided. Name string // If specified, this will be used as the group name for all non-error values returned // by the constructor. For more information on value groups, see the package documentation. // // A group option may not be provided if a name option is provided. // // Similar to group tags, the group name may be followed by a `,flatten` // option to indicate that each element in the slice returned by the // constructor should be injected into the value group individually. Group string // Target is the constructor or value being annotated with fx.Annotated. Target any } func (a Annotated) String() string { var fields []string if len(a.Name) > 0 { fields = append(fields, fmt.Sprintf("Name: %q", a.Name)) } if len(a.Group) > 0 { fields = append(fields, fmt.Sprintf("Group: %q", a.Group)) } if a.Target != nil { fields = append(fields, fmt.Sprintf("Target: %v", fxreflect.FuncName(a.Target))) } return fmt.Sprintf("fx.Annotated{%v}", strings.Join(fields, ", ")) } var ( // field used for embedding fx.In type in generated struct. _inAnnotationField = reflect.StructField{ Name: "In", Type: reflect.TypeOf(In{}), Anonymous: true, } // field used for embedding fx.Out type in generated struct. _outAnnotationField = reflect.StructField{ Name: "Out", Type: reflect.TypeOf(Out{}), Anonymous: true, } ) // Annotation specifies how to wrap a target for [Annotate]. // It can be used to set up additional options for a constructor, // or with [Supply], for a value. type Annotation interface { apply(*annotated) error build(*annotated) (any, error) } var ( _typeOfError = reflect.TypeOf((*error)(nil)).Elem() _nilError = reflect.Zero(_typeOfError) ) // annotationError is a wrapper for an error that was encountered while // applying annotation to a function. It contains the specific error // that it encountered as well as the target interface that was attempted // to be annotated. type annotationError struct { target any err error } func (e *annotationError) Error() string { return e.err.Error() } // Unwrap the wrapped error. func (e *annotationError) Unwrap() error { return e.err } type paramTagsAnnotation struct { tags []string } var _ Annotation = paramTagsAnnotation{} var ( errTagSyntaxSpace = errors.New(`multiple tags are not separated by space`) errTagKeySyntax = errors.New("tag key is invalid, Use group, name or optional as tag keys") errTagValueSyntaxQuote = errors.New(`tag value should start with double quote. i.e. key:"value" `) errTagValueSyntaxEndingQuote = errors.New(`tag value should end in double quote. i.e. key:"value" `) ) // Collections of key value pairs within a tag should be separated by a space. // Eg: `group:"some" optional:"true"`. func verifyTagsSpaceSeparated(tagIdx int, tag string) error { if tagIdx > 0 && tag != "" && tag[0] != ' ' { return errTagSyntaxSpace } return nil } // verify tag values are delimited with double quotes. func verifyValueQuote(value string) (string, error) { // starting quote should be a double quote if value[0] != '"' { return "", errTagValueSyntaxQuote } // validate tag value is within quotes i := 1 for i < len(value) && value[i] != '"' { if value[i] == '\\' { i++ } i++ } if i >= len(value) { return "", errTagValueSyntaxEndingQuote } return value[i+1:], nil } // Check whether the tag follows valid struct. // format and returns an error if it's invalid. (i.e. not following // tag:"value" space-separated list ) // Currently dig accepts only 'name', 'group', 'optional' as valid tag keys. func verifyAnnotateTag(tag string) error { tagIdx := 0 validKeys := map[string]struct{}{"group": {}, "optional": {}, "name": {}} for ; tag != ""; tagIdx++ { if err := verifyTagsSpaceSeparated(tagIdx, tag); err != nil { return err } i := 0 if strings.TrimSpace(tag) == "" { return nil } // parsing the key i.e. till reaching colon : for i < len(tag) && tag[i] != ':' { i++ } key := strings.TrimSpace(tag[:i]) if _, ok := validKeys[key]; !ok { return errTagKeySyntax } value, err := verifyValueQuote(tag[i+1:]) if err != nil { return err } tag = value } return nil } // Given func(T1, T2, T3, ..., TN), this generates a type roughly // equivalent to, // // struct { // fx.In // // Field1 T1 `$tags[0]` // Field2 T2 `$tags[1]` // ... // FieldN TN `$tags[N-1]` // } // // If there has already been a ParamTag that was applied, this // will return an error. // // If the tag is invalid and has mismatched quotation for example, // (`tag_name:"tag_value') , this will return an error. func (pt paramTagsAnnotation) apply(ann *annotated) error { if len(ann.ParamTags) > 0 { return errors.New("cannot apply more than one line of ParamTags") } for _, tag := range pt.tags { if err := verifyAnnotateTag(tag); err != nil { return err } } ann.ParamTags = pt.tags return nil } // build builds and returns a constructor after applying a ParamTags annotation func (pt paramTagsAnnotation) build(ann *annotated) (any, error) { paramTypes, remap := pt.parameters(ann) resultTypes, _ := ann.currentResultTypes() origFn := reflect.ValueOf(ann.Target) newFnType := reflect.FuncOf(paramTypes, resultTypes, false) newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) []reflect.Value { args = remap(args) return origFn.Call(args) }) return newFn.Interface(), nil } // parameters returns the type for the parameters of the annotated function, // and a function that maps the arguments of the annotated function // back to the arguments of the target function. func (pt paramTagsAnnotation) parameters(ann *annotated) ( types []reflect.Type, remap func([]reflect.Value) []reflect.Value, ) { ft := reflect.TypeOf(ann.Target) types = make([]reflect.Type, ft.NumIn()) for i := 0; i < ft.NumIn(); i++ { types[i] = ft.In(i) } // No parameter annotations. Return the original types // and an identity function. if len(pt.tags) == 0 { return types, func(args []reflect.Value) []reflect.Value { return args } } // Turn parameters into an fx.In struct. inFields := []reflect.StructField{_inAnnotationField} // there was a variadic argument, so it was pre-transformed if len(types) > 0 && isIn(types[0]) { paramType := types[0] for i := 1; i < paramType.NumField(); i++ { origField := paramType.Field(i) field := reflect.StructField{ Name: origField.Name, Type: origField.Type, Tag: origField.Tag, } if i-1 < len(pt.tags) { field.Tag = reflect.StructTag(pt.tags[i-1]) } inFields = append(inFields, field) } types = []reflect.Type{reflect.StructOf(inFields)} return types, func(args []reflect.Value) []reflect.Value { param := args[0] args[0] = reflect.New(paramType).Elem() for i := 1; i < paramType.NumField(); i++ { args[0].Field(i).Set(param.Field(i)) } return args } } for i, t := range types { field := reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: t, } if i < len(pt.tags) { field.Tag = reflect.StructTag(pt.tags[i]) } inFields = append(inFields, field) } types = []reflect.Type{reflect.StructOf(inFields)} return types, func(args []reflect.Value) []reflect.Value { params := args[0] args = args[:0] for i := 0; i < ft.NumIn(); i++ { args = append(args, params.Field(i+1)) } return args } } // ParamTags is an Annotation that annotates the parameter(s) of a function. // // When multiple tags are specified, each tag is mapped to the corresponding // positional parameter. // For example, the following will refer to a named database connection, // and the default, unnamed logger: // // fx.Annotate(func(log *log.Logger, conn *sql.DB) *Handler { // // ... // }, fx.ParamTags("", `name:"ro"`)) // // ParamTags cannot be used in a function that takes an fx.In struct as a // parameter. func ParamTags(tags ...string) Annotation { return paramTagsAnnotation{tags} } type resultTagsAnnotation struct { tags []string } var _ Annotation = resultTagsAnnotation{} // Given func(T1, T2, T3, ..., TN), this generates a type roughly // equivalent to, // // struct { // fx.Out // // Field1 T1 `$tags[0]` // Field2 T2 `$tags[1]` // ... // FieldN TN `$tags[N-1]` // } // // If there has already been a ResultTag that was applied, this // will return an error. // // If the tag is invalid and has mismatched quotation for example, // (`tag_name:"tag_value') , this will return an error. func (rt resultTagsAnnotation) apply(ann *annotated) error { if len(ann.ResultTags) > 0 { return errors.New("cannot apply more than one line of ResultTags") } for _, tag := range rt.tags { if err := verifyAnnotateTag(tag); err != nil { return err } } ann.ResultTags = rt.tags return nil } // build builds and returns a constructor after applying a ResultTags annotation func (rt resultTagsAnnotation) build(ann *annotated) (any, error) { paramTypes := ann.currentParamTypes() resultTypes, remapResults := rt.results(ann) origFn := reflect.ValueOf(ann.Target) newFnType := reflect.FuncOf(paramTypes, resultTypes, false) newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) []reflect.Value { results := origFn.Call(args) return remapResults(results) }) return newFn.Interface(), nil } // results returns the types of the results of the annotated function, // and a function that maps the results of the target function, // into a result compatible with the annotated function. func (rt resultTagsAnnotation) results(ann *annotated) ( types []reflect.Type, remap func([]reflect.Value) []reflect.Value, ) { types, hasError := ann.currentResultTypes() if hasError { types = types[:len(types)-1] } // No result annotations. Return the original types // and an identity function. if len(rt.tags) == 0 { return types, func(results []reflect.Value) []reflect.Value { return results } } // if there's no Out struct among the return types, there was no As annotation applied // just replace original result types with an Out struct and apply tags var ( newOut outStructInfo existingOuts []reflect.Type ) newOut.Fields = []reflect.StructField{_outAnnotationField} newOut.Offsets = []int{} for i, t := range types { if !isOut(t) { // this must be from the original function. // apply the tags field := reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: t, } if i < len(rt.tags) { field.Tag = reflect.StructTag(rt.tags[i]) } newOut.Offsets = append(newOut.Offsets, len(newOut.Fields)) newOut.Fields = append(newOut.Fields, field) continue } // this must be from an As annotation // apply the tags to the existing type taggedFields := make([]reflect.StructField, t.NumField()) taggedFields[0] = _outAnnotationField for j, tag := range rt.tags { if j+1 < t.NumField() { field := t.Field(j + 1) taggedFields[j+1] = reflect.StructField{ Name: field.Name, Type: field.Type, Tag: reflect.StructTag(tag), } } } existingOuts = append(existingOuts, reflect.StructOf(taggedFields)) } resType := reflect.StructOf(newOut.Fields) outTypes := []reflect.Type{resType} // append existing outs back to outTypes outTypes = append(outTypes, existingOuts...) if hasError { outTypes = append(outTypes, _typeOfError) } return outTypes, func(results []reflect.Value) []reflect.Value { var ( outErr error outResults []reflect.Value ) outResults = append(outResults, reflect.New(resType).Elem()) tIdx := 0 for i, r := range results { if i == len(results)-1 && hasError { // If hasError and this is the last item, // we are guaranteed that this is an error // object. if err, _ := r.Interface().(error); err != nil { outErr = err } continue } if i < len(newOut.Offsets) { if fieldIdx := newOut.Offsets[i]; fieldIdx > 0 { // fieldIdx 0 is an invalid index // because it refers to uninitialized // outs and would point to fx.Out in the // struct definition. We need to check this // to prevent panic from setting fx.Out to // a value. outResults[0].Field(fieldIdx).Set(r) } continue } if isOut(r.Type()) { tIdx++ if tIdx < len(outTypes) { newResult := reflect.New(outTypes[tIdx]).Elem() for j := 1; j < outTypes[tIdx].NumField(); j++ { newResult.Field(j).Set(r.Field(j)) } outResults = append(outResults, newResult) } } } if hasError { if outErr != nil { outResults = append(outResults, reflect.ValueOf(outErr)) } else { outResults = append(outResults, _nilError) } } return outResults } } // ResultTags is an Annotation that annotates the result(s) of a function. // When multiple tags are specified, each tag is mapped to the corresponding // positional result. // // For example, the following will produce a named database connection. // // fx.Annotate(func() (*sql.DB, error) { // // ... // }, fx.ResultTags(`name:"ro"`)) // // ResultTags cannot be used on a function that returns an fx.Out struct. func ResultTags(tags ...string) Annotation { return resultTagsAnnotation{tags} } type outStructInfo struct { Fields []reflect.StructField // fields of the struct Offsets []int // Offsets[i] is the index of result i in Fields } type _lifecycleHookAnnotationType int const ( _unknownHookType _lifecycleHookAnnotationType = iota _onStartHookType _onStopHookType ) type lifecycleHookAnnotation struct { Type _lifecycleHookAnnotationType Target any } var _ Annotation = (*lifecycleHookAnnotation)(nil) func (la *lifecycleHookAnnotation) String() string { name := "UnknownHookAnnotation" switch la.Type { case _onStartHookType: name = _onStartHook case _onStopHookType: name = _onStopHook } return name } func (la *lifecycleHookAnnotation) apply(ann *annotated) error { if la.Target == nil { return fmt.Errorf( "cannot use nil function for %q hook annotation", la, ) } for _, h := range ann.Hooks { if la.Type == h.Type { return fmt.Errorf( "cannot apply more than one %q hook annotation", la, ) } } ft := reflect.TypeOf(la.Target) if ft.Kind() != reflect.Func { return fmt.Errorf( "must provide function for %q hook, got %v (%T)", la, la.Target, la.Target, ) } if n := ft.NumOut(); n > 0 { if n > 1 || ft.Out(0) != _typeOfError { return fmt.Errorf( "optional hook return may only be an error, got %v (%T)", la.Target, la.Target, ) } } if ft.IsVariadic() { return fmt.Errorf( "hooks must not accept variadic parameters, got %v (%T)", la.Target, la.Target, ) } ann.Hooks = append(ann.Hooks, la) return nil } // build builds and returns a constructor after applying a lifecycle hook annotation. func (la *lifecycleHookAnnotation) build(ann *annotated) (any, error) { resultTypes, hasError := ann.currentResultTypes() if !hasError { resultTypes = append(resultTypes, _typeOfError) } hookInstaller, paramTypes, remapParams, err := la.buildHookInstaller(ann) if err != nil { return nil, err } origFn := reflect.ValueOf(ann.Target) newFnType := reflect.FuncOf(paramTypes, resultTypes, false) newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) []reflect.Value { // copy the original arguments before remapping the parameters // so that we can apply them to the hookInstaller. origArgs := make([]reflect.Value, len(args)) copy(origArgs, args) args = remapParams(args) results := origFn.Call(args) if hasError { errVal := results[len(results)-1] results = results[:len(results)-1] if err, _ := errVal.Interface().(error); err != nil { // if constructor returned error, do not call hook installer return append(results, errVal) } } hookInstallerResults := hookInstaller.Call(append(results, origArgs...)) results = append(results, hookInstallerResults[0]) return results }) return newFn.Interface(), nil } var ( _typeOfLifecycle = reflect.TypeOf((*Lifecycle)(nil)).Elem() _typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem() ) // validateHookDeps validates the dependencies of a hook function and returns true if the dependencies are valid. func (la *lifecycleHookAnnotation) validateHookDeps(hookParamTypes []reflect.Type, paramTypes []reflect.Type, resultTypes []reflect.Type) (err error) { type key struct { t reflect.Type name string group string } formatLog := func(k key) error { var tags []string if len(k.name) > 0 { tags = append(tags, fmt.Sprintf("name:\"%s\"", k.name)) } if len(k.group) > 0 { tags = append(tags, fmt.Sprintf("group:\"%s\"", k.group)) } var formatted string if len(tags) > 0 { formatted = fmt.Sprintf("%s `%s`", k.t.String(), strings.Join(tags, " ")) } else { formatted = k.t.String() } return fmt.Errorf("the %s hook function takes in a parameter of \"%s\", but the annotated function does not have parameters or results of that type", la.String(), formatted) } err = nil seen := make(map[key]struct{}) for _, t := range paramTypes { if !isIn(t) { seen[key{t: t}] = struct{}{} continue } for i := 1; i < t.NumField(); i++ { field := t.Field(i) seen[key{ t: field.Type, name: field.Tag.Get("name"), group: field.Tag.Get("group"), }] = struct{}{} } } for _, t := range resultTypes { if !isOut(t) { seen[key{t: t}] = struct{}{} continue } for i := 1; i < t.NumField(); i++ { field := t.Field(i) seen[key{ t: field.Type, name: field.Tag.Get("name"), group: field.Tag.Get("group"), }] = struct{}{} } } for _, t := range hookParamTypes { if !isIn(t) { k := key{t: t} if _, ok := seen[k]; !ok { err = formatLog(k) return } continue } for i := 1; i < t.NumField(); i++ { field := t.Field(i) k := key{ t: field.Type, name: field.Tag.Get("name"), group: field.Tag.Get("group"), } if _, ok := seen[k]; !ok { err = formatLog(k) return } } } return } // buildHookInstaller returns a function that appends a hook to Lifecycle when called, // along with the new parameter types and a function that maps arguments to the annotated constructor func (la *lifecycleHookAnnotation) buildHookInstaller(ann *annotated) ( hookInstaller reflect.Value, paramTypes []reflect.Type, remapParams func([]reflect.Value) []reflect.Value, // function to remap parameters to function being annotated err error, ) { paramTypes = ann.currentParamTypes() paramTypes, remapParams = injectLifecycle(paramTypes) resultTypes, hasError := ann.currentResultTypes() if hasError { resultTypes = resultTypes[:len(resultTypes)-1] } // look for the context.Context type from the original hook function // and then exclude it from the paramTypes of invokeFn because context.Context // will be injected by the lifecycle ctxPos := -1 ctxStructPos := -1 origHookFn := reflect.ValueOf(la.Target) origHookFnT := reflect.TypeOf(la.Target) invokeParamTypes := []reflect.Type{ _typeOfLifecycle, } for i := 0; i < origHookFnT.NumIn(); i++ { t := origHookFnT.In(i) if t == _typeOfContext && ctxPos < 0 { ctxPos = i continue } if !isIn(t) { invokeParamTypes = append(invokeParamTypes, origHookFnT.In(i)) continue } fields := []reflect.StructField{_inAnnotationField} for j := 1; j < t.NumField(); j++ { field := t.Field(j) if field.Type == _typeOfContext && ctxPos < 0 { ctxStructPos = i ctxPos = j continue } fields = append(fields, field) } invokeParamTypes = append(invokeParamTypes, reflect.StructOf(fields)) } if err = la.validateHookDeps(invokeParamTypes, paramTypes, resultTypes); err != nil { return } invokeFnT := reflect.FuncOf(invokeParamTypes, []reflect.Type{}, false) invokeFn := reflect.MakeFunc(invokeFnT, func(args []reflect.Value) (results []reflect.Value) { lc := args[0].Interface().(Lifecycle) args = args[1:] hookArgs := make([]reflect.Value, origHookFnT.NumIn()) hookFn := func(ctx context.Context) (err error) { // If the hook function has multiple parameters, and the first // parameter is a context, inject the provided context. if ctxStructPos < 0 { offset := 0 for i := range hookArgs { if i == ctxPos { hookArgs[i] = reflect.ValueOf(ctx) offset = 1 continue } if i-offset >= 0 && i-offset < len(args) { hookArgs[i] = args[i-offset] } } } else { for i := 0; i < origHookFnT.NumIn(); i++ { if i != ctxStructPos { hookArgs[i] = args[i] continue } t := origHookFnT.In(i) v := reflect.New(t).Elem() for j := 1; j < t.NumField(); j++ { if j < ctxPos { v.Field(j).Set(args[i].Field(j)) } else if j == ctxPos { v.Field(j).Set(reflect.ValueOf(ctx)) } else { v.Field(j).Set(args[i].Field(j - 1)) } } hookArgs[i] = v } } hookResults := origHookFn.Call(hookArgs) if len(hookResults) > 0 && hookResults[0].Type() == _typeOfError { err, _ = hookResults[0].Interface().(error) } return err } lc.Append(la.buildHook(hookFn)) return results }) installerType := reflect.FuncOf(append(resultTypes, paramTypes...), []reflect.Type{_typeOfError}, false) hookInstaller = reflect.MakeFunc(installerType, func(args []reflect.Value) (results []reflect.Value) { // build a private scope for hook function var scope *dig.Scope switch la.Type { case _onStartHookType: scope = ann.container.Scope("onStartHookScope") case _onStopHookType: scope = ann.container.Scope("onStopHookScope") } // provide the private scope with the current dependencies and results of the annotated function results = []reflect.Value{_nilError} ctor := makeHookScopeCtor(paramTypes, resultTypes, args) if err := scope.Provide(ctor); err != nil { results[0] = reflect.ValueOf(fmt.Errorf("error providing possible parameters for hook installer: %w", err)) return results } // invoking invokeFn appends the hook function to lifecycle if err := scope.Invoke(invokeFn.Interface()); err != nil { results[0] = reflect.ValueOf(fmt.Errorf("error invoking hook installer: %w", err)) return results } return results }) return hookInstaller, paramTypes, remapParams, nil } var ( _nameTag = "name" _groupTag = "group" ) // makeHookScopeCtor makes a constructor that provides all possible parameters // that the lifecycle hook being appended can depend on. It also deduplicates // duplicate param and result types, which is possible when using fx.Decorate, // and uses values from results for providing the deduplicated types. func makeHookScopeCtor(paramTypes []reflect.Type, resultTypes []reflect.Type, args []reflect.Value) any { type key struct { t reflect.Type name string group string } seen := map[key]struct{}{} outTypes := make([]reflect.Type, len(resultTypes)) for i, t := range resultTypes { outTypes[i] = t if isOut(t) { for j := 1; j < t.NumField(); j++ { field := t.Field(j) seen[key{ t: field.Type, name: field.Tag.Get(_nameTag), group: field.Tag.Get(_groupTag), }] = struct{}{} } continue } seen[key{t: t}] = struct{}{} } fields := []reflect.StructField{_outAnnotationField} skippedParams := make([][]int, len(paramTypes)) for i, t := range paramTypes { skippedParams[i] = []int{} if isIn(t) { for j := 1; j < t.NumField(); j++ { origField := t.Field(j) k := key{ t: origField.Type, name: origField.Tag.Get(_nameTag), group: origField.Tag.Get(_groupTag), } if _, ok := seen[k]; ok { skippedParams[i] = append(skippedParams[i], j) continue } field := reflect.StructField{ Name: fmt.Sprintf("Field%d", j-1), Type: origField.Type, Tag: origField.Tag, } fields = append(fields, field) } continue } k := key{t: t} if _, ok := seen[k]; ok { skippedParams[i] = append(skippedParams[i], i) continue } field := reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: t, } fields = append(fields, field) } outTypes = append(outTypes, reflect.StructOf(fields)) ctorType := reflect.FuncOf([]reflect.Type{}, outTypes, false) ctor := reflect.MakeFunc(ctorType, func(_ []reflect.Value) []reflect.Value { nOut := len(outTypes) results := make([]reflect.Value, nOut) for i := 0; i < nOut-1; i++ { results[i] = args[i] } v := reflect.New(outTypes[nOut-1]).Elem() fieldIdx := 1 for i := nOut - 1; i < len(args); i++ { paramIdx := i - (nOut - 1) if isIn(paramTypes[paramIdx]) { skippedIdx := 0 for j := 1; j < paramTypes[paramIdx].NumField(); j++ { if len(skippedParams[paramIdx]) > 0 && skippedParams[paramIdx][skippedIdx] == j { // skip skippedIdx++ continue } v.Field(fieldIdx).Set(args[i].Field(j)) fieldIdx++ } } else { if len(skippedParams[paramIdx]) > 0 && skippedParams[paramIdx][0] == paramIdx { continue } v.Field(fieldIdx).Set(args[i]) fieldIdx++ } } results[nOut-1] = v return results }) return ctor.Interface() } func injectLifecycle(paramTypes []reflect.Type) ([]reflect.Type, func([]reflect.Value) []reflect.Value) { // since lifecycle already exists in param types, no need to inject again if lifecycleExists(paramTypes) { return paramTypes, func(args []reflect.Value) []reflect.Value { return args } } // If params are tagged or there's an untagged variadic argument, // add a Lifecycle field to the param struct if len(paramTypes) > 0 && isIn(paramTypes[0]) { taggedParam := paramTypes[0] fields := []reflect.StructField{ taggedParam.Field(0), { Name: "Lifecycle", Type: _typeOfLifecycle, }, } for i := 1; i < taggedParam.NumField(); i++ { fields = append(fields, taggedParam.Field(i)) } newParamType := reflect.StructOf(fields) return []reflect.Type{newParamType}, func(args []reflect.Value) []reflect.Value { param := args[0] args[0] = reflect.New(taggedParam).Elem() for i := 1; i < taggedParam.NumField(); i++ { args[0].Field(i).Set(param.Field(i + 1)) } return args } } return append([]reflect.Type{_typeOfLifecycle}, paramTypes...), func(args []reflect.Value) []reflect.Value { return args[1:] } } func lifecycleExists(paramTypes []reflect.Type) bool { for _, t := range paramTypes { if t == _typeOfLifecycle { return true } if isIn(t) { for i := 1; i < t.NumField(); i++ { if t.Field(i).Type == _typeOfLifecycle { return true } } } } return false } func (la *lifecycleHookAnnotation) buildHook(fn func(context.Context) error) (hook Hook) { switch la.Type { case _onStartHookType: hook.OnStart = fn case _onStopHookType: hook.OnStop = fn } return hook } // OnStart is an Annotation that appends an OnStart Hook to the application // Lifecycle when that function is called. This provides a way to create // Lifecycle OnStart (see Lifecycle type documentation) hooks without building a // function that takes a dependency on the Lifecycle type. // // fx.Provide( // fx.Annotate( // NewServer, // fx.OnStart(func(ctx context.Context, server Server) error { // return server.Listen(ctx) // }), // ) // ) // // Which is functionally the same as: // // fx.Provide( // func(lifecycle fx.Lifecycle, p Params) Server { // server := NewServer(p) // lifecycle.Append(fx.Hook{ // OnStart: func(ctx context.Context) error { // return server.Listen(ctx) // }, // }) // return server // } // ) // // It is also possible to use OnStart annotation with other parameter and result // annotations, provided that the parameter of the function passed to OnStart // matches annotated parameters and results. // // For example, the following is possible: // // fx.Provide( // fx.Annotate( // func (a A) B {...}, // fx.ParamTags(`name:"A"`), // fx.ResultTags(`name:"B"`), // fx.OnStart(func (p OnStartParams) {...}), // ), // ) // // As long as OnStartParams looks like the following and has no other dependencies // besides Context or Lifecycle: // // type OnStartParams struct { // fx.In // FieldA A `name:"A"` // FieldB B `name:"B"` // } // // Only one OnStart annotation may be applied to a given function at a time, // however functions may be annotated with other types of lifecycle Hooks, such // as OnStop. The hook function passed into OnStart cannot take any arguments // outside of the annotated constructor's existing dependencies or results, except // a context.Context. func OnStart(onStart any) Annotation { return &lifecycleHookAnnotation{ Type: _onStartHookType, Target: onStart, } } // OnStop is an Annotation that appends an OnStop Hook to the application // Lifecycle when that function is called. This provides a way to create // Lifecycle OnStop (see Lifecycle type documentation) hooks without building a // function that takes a dependency on the Lifecycle type. // // fx.Provide( // fx.Annotate( // NewServer, // fx.OnStop(func(ctx context.Context, server Server) error { // return server.Shutdown(ctx) // }), // ) // ) // // Which is functionally the same as: // // fx.Provide( // func(lifecycle fx.Lifecycle, p Params) Server { // server := NewServer(p) // lifecycle.Append(fx.Hook{ // OnStop: func(ctx context.Context) error { // return server.Shutdown(ctx) // }, // }) // return server // } // ) // // It is also possible to use OnStop annotation with other parameter and result // annotations, provided that the parameter of the function passed to OnStop // matches annotated parameters and results. // // For example, the following is possible: // // fx.Provide( // fx.Annotate( // func (a A) B {...}, // fx.ParamTags(`name:"A"`), // fx.ResultTags(`name:"B"`), // fx.OnStop(func (p OnStopParams) {...}), // ), // ) // // As long as OnStopParams looks like the following and has no other dependencies // besides Context or Lifecycle: // // type OnStopParams struct { // fx.In // FieldA A `name:"A"` // FieldB B `name:"B"` // } // // Only one OnStop annotation may be applied to a given function at a time, // however functions may be annotated with other types of lifecycle Hooks, such // as OnStart. The hook function passed into OnStop cannot take any arguments // outside of the annotated constructor's existing dependencies or results, except // a context.Context. func OnStop(onStop any) Annotation { return &lifecycleHookAnnotation{ Type: _onStopHookType, Target: onStop, } } type asAnnotation struct { targets []any types []asType } type asType struct { self bool typ reflect.Type // May be nil if self is true. } func (a asType) String() string { if a.self { return "self" } return a.typ.String() } func isOut(t reflect.Type) bool { return (t.Kind() == reflect.Struct && dig.IsOut(reflect.New(t).Elem().Interface())) } func isIn(t reflect.Type) bool { return (t.Kind() == reflect.Struct && dig.IsIn(reflect.New(t).Elem().Interface())) } var _ Annotation = (*asAnnotation)(nil) // As is an Annotation that annotates the result of a function (i.e. a // constructor) to be provided as another interface. // // For example, the following code specifies that the return type of // bytes.NewBuffer (bytes.Buffer) should be provided as io.Writer type: // // fx.Provide( // fx.Annotate(bytes.NewBuffer, fx.As(new(io.Writer))) // ) // // In other words, the code above is equivalent to: // // fx.Provide(func() io.Writer { // return bytes.NewBuffer() // // provides io.Writer instead of *bytes.Buffer // }) // // Note that the bytes.Buffer type is provided as an io.Writer type, so this // constructor does NOT provide both bytes.Buffer and io.Writer type; it just // provides io.Writer type. // // When multiple values are returned by the annotated function, each type // gets mapped to corresponding positional result of the annotated function. // // For example, // // func a() (bytes.Buffer, bytes.Buffer) { // ... // } // fx.Provide( // fx.Annotate(a, fx.As(new(io.Writer), new(io.Reader))) // ) // // Is equivalent to, // // fx.Provide(func() (io.Writer, io.Reader) { // w, r := a() // return w, r // } // // As entirely replaces the default return types of a function. In order // to maintain the original return types when using As, see [Self]. // // As annotation cannot be used in a function that returns an [Out] struct as a return type. func As(interfaces ...any) Annotation { return &asAnnotation{targets: interfaces} } // Self returns a special value that can be passed to [As] to indicate // that a type should be provided as its original type, in addition to whatever other // types it gets provided as via other [As] annotations. // // For example, // // fx.Provide( // fx.Annotate( // bytes.NewBuffer, // fx.As(new(io.Writer)), // fx.As(fx.Self()), // ) // ) // // Is equivalent to, // // fx.Provide( // bytes.NewBuffer, // func(b *bytes.Buffer) io.Writer { // return b // }, // ) // // in that it provides the same *bytes.Buffer instance // as both a *bytes.Buffer and an io.Writer. func Self() any { return &self{} } type self struct{} func (at *asAnnotation) apply(ann *annotated) error { at.types = make([]asType, len(at.targets)) for i, typ := range at.targets { if _, ok := typ.(*self); ok { at.types[i] = asType{self: true} continue } t := reflect.TypeOf(typ) if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Interface { return fmt.Errorf("fx.As: argument must be a pointer to an interface: got %v", t) } t = t.Elem() at.types[i] = asType{typ: t} } ann.As = append(ann.As, at.types) return nil } // build implements Annotation func (at *asAnnotation) build(ann *annotated) (any, error) { paramTypes := ann.currentParamTypes() resultTypes, remapResults, err := at.results(ann) if err != nil { return nil, err } origFn := reflect.ValueOf(ann.Target) newFnType := reflect.FuncOf(paramTypes, resultTypes, false) newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) []reflect.Value { results := origFn.Call(args) return remapResults(results) }) return newFn.Interface(), nil } func (at *asAnnotation) results(ann *annotated) ( types []reflect.Type, remap func([]reflect.Value) []reflect.Value, err error, ) { types, hasError := ann.currentResultTypes() fields := []reflect.StructField{_outAnnotationField} if hasError { types = types[:len(types)-1] } resultFields, getResult := extractResultFields(types) for i, f := range resultFields { t := f.Type field := reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: t, Tag: f.Tag, } if i >= len(at.types) || at.types[i].self { fields = append(fields, field) continue } if !t.Implements(at.types[i].typ) { return nil, nil, fmt.Errorf("invalid fx.As: %v does not implement %v", t, at.types[i]) } field.Type = at.types[i].typ fields = append(fields, field) } resType := reflect.StructOf(fields) var outTypes []reflect.Type outTypes = append(types, resType) if hasError { outTypes = append(outTypes, _typeOfError) } return outTypes, func(results []reflect.Value) []reflect.Value { var ( outErr error outResults []reflect.Value ) for i, r := range results { if i == len(results)-1 && hasError { // If hasError and this is the last item, // we are guaranteed that this is an error // object. if err, _ := r.Interface().(error); err != nil { outErr = err } continue } outResults = append(outResults, r) } newOutResult := reflect.New(resType).Elem() for i := 1; i < resType.NumField(); i++ { newOutResult.Field(i).Set(getResult(i, results)) } outResults = append(outResults, newOutResult) if hasError { if outErr != nil { outResults = append(outResults, reflect.ValueOf(outErr)) } else { outResults = append(outResults, _nilError) } } return outResults }, nil } func extractResultFields(types []reflect.Type) ([]reflect.StructField, func(int, []reflect.Value) reflect.Value) { var resultFields []reflect.StructField if len(types) > 0 && isOut(types[0]) { for i := 1; i < types[0].NumField(); i++ { resultFields = append(resultFields, types[0].Field(i)) } return resultFields, func(idx int, results []reflect.Value) reflect.Value { return results[0].Field(idx) } } for i, t := range types { if isOut(t) { continue } field := reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: t, } resultFields = append(resultFields, field) } return resultFields, func(idx int, results []reflect.Value) reflect.Value { return results[idx-1] } } type fromAnnotation struct { targets []any types []reflect.Type } var _ Annotation = (*fromAnnotation)(nil) // From is an [Annotation] that annotates the parameter(s) for a function (i.e. a // constructor) to be accepted from other provided types. It is analogous to the // [As] for parameter types to the constructor. // // For example, // // type Runner interface { Run() } // func NewFooRunner() *FooRunner // implements Runner // func NewRunnerWrap(r Runner) *RunnerWrap // // fx.Provide( // fx.Annotate( // NewRunnerWrap, // fx.From(new(*FooRunner)), // ), // ) // // Is equivalent to, // // fx.Provide(func(r *FooRunner) *RunnerWrap { // // need *FooRunner instead of Runner // return NewRunnerWrap(r) // }) // // When the annotated function takes in multiple parameters, each type gets // mapped to corresponding positional parameter of the annotated function // // For example, // // func NewBarRunner() *BarRunner // implements Runner // func NewRunnerWraps(r1 Runner, r2 Runner) *RunnerWraps // // fx.Provide( // fx.Annotate( // NewRunnerWraps, // fx.From(new(*FooRunner), new(*BarRunner)), // ), // ) // // Is equivalent to, // // fx.Provide(func(r1 *FooRunner, r2 *BarRunner) *RunnerWraps { // return NewRunnerWraps(r1, r2) // }) // // From annotation cannot be used in a function that takes an [In] struct as a // parameter. func From(interfaces ...any) Annotation { return &fromAnnotation{targets: interfaces} } func (fr *fromAnnotation) apply(ann *annotated) error { if len(ann.From) > 0 { return errors.New("cannot apply more than one line of From") } ft := reflect.TypeOf(ann.Target) fr.types = make([]reflect.Type, len(fr.targets)) for i, typ := range fr.targets { if ft.IsVariadic() && i == ft.NumIn()-1 { return errors.New("fx.From: cannot annotate a variadic argument") } t := reflect.TypeOf(typ) if t == nil || t.Kind() != reflect.Ptr { return fmt.Errorf("fx.From: argument must be a pointer to a type that implements some interface: got %v", t) } fr.types[i] = t.Elem() } ann.From = fr.types return nil } // build builds and returns a constructor after applying a From annotation func (fr *fromAnnotation) build(ann *annotated) (any, error) { paramTypes, remap, err := fr.parameters(ann) if err != nil { return nil, err } resultTypes, _ := ann.currentResultTypes() origFn := reflect.ValueOf(ann.Target) newFnType := reflect.FuncOf(paramTypes, resultTypes, false) newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) []reflect.Value { args = remap(args) return origFn.Call(args) }) return newFn.Interface(), nil } // parameters returns the type for the parameters of the annotated function, // and a function that maps the arguments of the annotated function // back to the arguments of the target function. func (fr *fromAnnotation) parameters(ann *annotated) ( types []reflect.Type, remap func([]reflect.Value) []reflect.Value, err error, ) { ft := reflect.TypeOf(ann.Target) types = make([]reflect.Type, ft.NumIn()) for i := 0; i < ft.NumIn(); i++ { types[i] = ft.In(i) } // No parameter annotations. Return the original types // and an identity function. if len(fr.targets) == 0 { return types, func(args []reflect.Value) []reflect.Value { return args }, nil } // Turn parameters into an fx.In struct. inFields := []reflect.StructField{_inAnnotationField} // The following situations may occur: // 1. there was a variadic argument, so it was pre-transformed. // 2. another parameter annotation was transformed (ex: ParamTags). // so need to visit fields of the fx.In struct. if len(types) > 0 && isIn(types[0]) { paramType := types[0] for i := 1; i < paramType.NumField(); i++ { origField := paramType.Field(i) field := reflect.StructField{ Name: origField.Name, Type: origField.Type, Tag: origField.Tag, } if i-1 < len(fr.types) { t := fr.types[i-1] if !t.Implements(field.Type) { return nil, nil, fmt.Errorf("invalid fx.From: %v does not implement %v", t, field.Type) } field.Type = t } inFields = append(inFields, field) } types = []reflect.Type{reflect.StructOf(inFields)} return types, func(args []reflect.Value) []reflect.Value { param := args[0] args[0] = reflect.New(paramType).Elem() for i := 1; i < paramType.NumField(); i++ { args[0].Field(i).Set(param.Field(i)) } return args }, nil } for i, t := range types { field := reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: t, } if i < len(fr.types) { t := fr.types[i] if !t.Implements(field.Type) { return nil, nil, fmt.Errorf("invalid fx.From: %v does not implement %v", t, field.Type) } field.Type = t } inFields = append(inFields, field) } types = []reflect.Type{reflect.StructOf(inFields)} return types, func(args []reflect.Value) []reflect.Value { params := args[0] args = args[:0] for i := 0; i < ft.NumIn(); i++ { args = append(args, params.Field(i+1)) } return args }, nil } type annotated struct { Target any Annotations []Annotation ParamTags []string ResultTags []string As [][]asType From []reflect.Type FuncPtr uintptr Hooks []*lifecycleHookAnnotation // container is used to build private scopes for lifecycle hook functions // added via fx.OnStart and fx.OnStop annotations. container *dig.Container } func (ann annotated) String() string { var sb strings.Builder sb.WriteString("fx.Annotate(") sb.WriteString(fxreflect.FuncName(ann.Target)) if tags := ann.ParamTags; len(tags) > 0 { fmt.Fprintf(&sb, ", fx.ParamTags(%q)", tags) } if tags := ann.ResultTags; len(tags) > 0 { fmt.Fprintf(&sb, ", fx.ResultTags(%q)", tags) } if as := ann.As; len(as) > 0 { fmt.Fprintf(&sb, ", fx.As(%v)", as) } if from := ann.From; len(from) > 0 { fmt.Fprintf(&sb, ", fx.From(%v)", from) } return sb.String() } // Build builds and returns a constructor based on fx.In/fx.Out params and // results wrapping the original constructor passed to fx.Annotate. func (ann *annotated) Build() (any, error) { ann.container = dig.New() ft := reflect.TypeOf(ann.Target) if ft.Kind() != reflect.Func { return nil, fmt.Errorf("must provide constructor function, got %v (%T)", ann.Target, ann.Target) } if err := ann.typeCheckOrigFn(); err != nil { return nil, fmt.Errorf("invalid annotation function %T: %w", ann.Target, err) } ann.applyOptionalTag() var ( err error lcHookAnns []*lifecycleHookAnnotation ) for _, annotation := range ann.Annotations { if lcHookAnn, ok := annotation.(*lifecycleHookAnnotation); ok { lcHookAnns = append(lcHookAnns, lcHookAnn) continue } if ann.Target, err = annotation.build(ann); err != nil { return nil, err } } // need to call cleanUpAsResults before applying lifecycle annotations // to exclude the original results from the hook's scope if any // fx.As annotations were applied ann.cleanUpAsResults() for _, la := range lcHookAnns { if ann.Target, err = la.build(ann); err != nil { return nil, err } } return ann.Target, nil } // applyOptionalTag checks if function being annotated is variadic // and applies optional tag to the variadic argument before // applying any other annotations func (ann *annotated) applyOptionalTag() { ft := reflect.TypeOf(ann.Target) if !ft.IsVariadic() { return } resultTypes, _ := ann.currentResultTypes() fields := []reflect.StructField{_inAnnotationField} for i := 0; i < ft.NumIn(); i++ { field := reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: ft.In(i), } if i == ft.NumIn()-1 { // Mark a variadic argument optional by default // so that just wrapping a function in fx.Annotate does not // suddenly introduce a required []arg dependency. field.Tag = reflect.StructTag(`optional:"true"`) } fields = append(fields, field) } paramType := reflect.StructOf(fields) origFn := reflect.ValueOf(ann.Target) newFnType := reflect.FuncOf([]reflect.Type{paramType}, resultTypes, false) newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) []reflect.Value { params := args[0] args = args[:0] for i := 0; i < ft.NumIn(); i++ { args = append(args, params.Field(i+1)) } return origFn.CallSlice(args) }) ann.Target = newFn.Interface() } // cleanUpAsResults does a check to see if an As annotation was applied. // If there was any fx.As annotation applied, cleanUpAsResults wraps the // function one more time to remove the results from the original function. func (ann *annotated) cleanUpAsResults() { // clean up orig function results if there were any As annotations if len(ann.As) < 1 { return } paramTypes := ann.currentParamTypes() resultTypes, hasError := ann.currentResultTypes() numRes := len(ann.As) if hasError { numRes++ } newResultTypes := resultTypes[len(resultTypes)-numRes:] origFn := reflect.ValueOf(ann.Target) newFnType := reflect.FuncOf(paramTypes, newResultTypes, false) newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) (results []reflect.Value) { results = origFn.Call(args) results = results[len(results)-numRes:] return }) ann.Target = newFn.Interface() } // checks and returns a non-nil error if the target function: // - returns an fx.Out struct as a result and has either a ResultTags or an As annotation // - takes in an fx.In struct as a parameter and has either a ParamTags or a From annotation // - has an error result not as the last result. func (ann *annotated) typeCheckOrigFn() error { ft := reflect.TypeOf(ann.Target) numOut := ft.NumOut() for i := range numOut { ot := ft.Out(i) if ot == _typeOfError && i != numOut-1 { return fmt.Errorf( "only the last result can be an error: "+ "%v (%v) returns error as result %d", fxreflect.FuncName(ann.Target), ft, i) } if ot.Kind() != reflect.Struct { continue } if !dig.IsOut(reflect.New(ft.Out(i)).Elem().Interface()) { continue } if len(ann.ResultTags) > 0 || len(ann.As) > 0 { return errors.New("fx.Out structs cannot be annotated with fx.ResultTags or fx.As") } } for i := 0; i < ft.NumIn(); i++ { it := ft.In(i) if it.Kind() != reflect.Struct { continue } if !dig.IsIn(reflect.New(ft.In(i)).Elem().Interface()) { continue } if len(ann.ParamTags) > 0 || len(ann.From) > 0 { return errors.New("fx.In structs cannot be annotated with fx.ParamTags or fx.From") } } return nil } func (ann *annotated) currentResultTypes() (resultTypes []reflect.Type, hasError bool) { ft := reflect.TypeOf(ann.Target) numOut := ft.NumOut() resultTypes = make([]reflect.Type, numOut) for i := range numOut { resultTypes[i] = ft.Out(i) if resultTypes[i] == _typeOfError && i == numOut-1 { hasError = true } } return resultTypes, hasError } func (ann *annotated) currentParamTypes() []reflect.Type { ft := reflect.TypeOf(ann.Target) paramTypes := make([]reflect.Type, ft.NumIn()) for i := 0; i < ft.NumIn(); i++ { paramTypes[i] = ft.In(i) } return paramTypes } // Annotate lets you annotate a function's parameters and returns // without you having to declare separate struct definitions for them. // // For example, // // func NewGateway(ro, rw *db.Conn) *Gateway { ... } // fx.Provide( // fx.Annotate( // NewGateway, // fx.ParamTags(`name:"ro" optional:"true"`, `name:"rw"`), // fx.ResultTags(`name:"foo"`), // ), // ) // // Is equivalent to, // // type params struct { // fx.In // // RO *db.Conn `name:"ro" optional:"true"` // RW *db.Conn `name:"rw"` // } // // type result struct { // fx.Out // // GW *Gateway `name:"foo"` // } // // fx.Provide(func(p params) result { // return result{GW: NewGateway(p.RO, p.RW)} // }) // // Using the same annotation multiple times is invalid. // For example, the following will fail with an error: // // fx.Provide( // fx.Annotate( // NewGateWay, // fx.ParamTags(`name:"ro" optional:"true"`), // fx.ParamTags(`name:"rw"), // ERROR: ParamTags was already used above // fx.ResultTags(`name:"foo"`) // ) // ) // // If more tags are given than the number of parameters/results, only // the ones up to the number of parameters/results will be applied. // // # Variadic functions // // If the provided function is variadic, Annotate treats its parameter as a // slice. For example, // // fx.Annotate(func(w io.Writer, rs ...io.Reader) { // // ... // }, ...) // // Is equivalent to, // // fx.Annotate(func(w io.Writer, rs []io.Reader) { // // ... // }, ...) // // You can use variadic parameters with Fx's value groups. // For example, // // fx.Annotate(func(mux *http.ServeMux, handlers ...http.Handler) { // // ... // }, fx.ParamTags(``, `group:"server"`)) // // If we provide the above to the application, // any constructor in the Fx application can inject its HTTP handlers // by using [Annotate], [Annotated], or [Out]. // // fx.Annotate( // func(..) http.Handler { ... }, // fx.ResultTags(`group:"server"`), // ) // // fx.Annotated{ // Target: func(..) http.Handler { ... }, // Group: "server", // } func Annotate(t any, anns ...Annotation) any { result := annotated{Target: t} for _, ann := range anns { if err := ann.apply(&result); err != nil { return annotationError{ target: t, err: err, } } } result.Annotations = anns return result } ================================================ FILE: annotated_test.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fx_test import ( "bytes" "context" "errors" "fmt" "io" "strconv" "strings" "sync/atomic" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/fx/fxtest" ) func TestAnnotated(t *testing.T) { t.Parallel() type a struct { name string } type in struct { fx.In A *a `name:"foo"` } newA := func() *a { return &a{name: "foo"} } t.Run("Provide", func(t *testing.T) { t.Parallel() var in in app := fxtest.New(t, fx.Provide( fx.Annotated{ Name: "foo", Target: newA, }, ), fx.Populate(&in), ) defer app.RequireStart().RequireStop() assert.NotNil(t, in.A, "expected in.A to be injected") assert.Equal(t, "foo", in.A.name, "expected to get a type 'a' of name 'foo'") }) } type fromStringer struct { name string } func (from *fromStringer) String() string { return from.name } type asStringer struct { name string } func (as *asStringer) String() string { return as.name } type anotherStringer struct { name string } func (a anotherStringer) String() string { return a.name } func TestAnnotatedFrom(t *testing.T) { t.Parallel() type myStringer interface { String() string } newFromStringer := func() *fromStringer { return &fromStringer{ name: "a good stringer", } } tests := []struct { desc string provide fx.Option invoke any }{ { desc: "provide a good stringer", provide: fx.Provide( newFromStringer, fx.Annotate( func(myStringer myStringer) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.From(new(*fromStringer)), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "a good stringer") }, }, { desc: "value type implementing interface", provide: fx.Provide( func() anotherStringer { return anotherStringer{ "another stringer", } }, fx.Annotate( func(myStringer myStringer) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.From(new(anotherStringer)), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "another stringer") }, }, { desc: "provide with multiple types From", provide: fx.Provide( newFromStringer, func() anotherStringer { return anotherStringer{ "another stringer", } }, fx.Annotate( func(myStringer1 myStringer, myStringer2 myStringer) fmt.Stringer { return &fromStringer{ name: myStringer1.String() + " and " + myStringer2.String(), } }, fx.From(new(anotherStringer), new(*fromStringer)), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "another stringer and a good stringer") }, }, { desc: "provide from with param annotation", provide: fx.Provide( fx.Annotate( newFromStringer, fx.ResultTags(`name:"struct1"`), ), fx.Annotate( func(myStringer myStringer) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.From(new(*fromStringer)), fx.ParamTags(`name:"struct1"`), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "a good stringer") }, }, { // same as the test above, except now we annotate // it in a different order. desc: "provide from with param annotation, in different order", provide: fx.Provide( fx.Annotate( newFromStringer, fx.ResultTags(`name:"struct1"`), ), fx.Annotate( func(myStringer myStringer) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.ParamTags(`name:"struct1"`), fx.From(new(*fromStringer)), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "a good stringer") }, }, { desc: "annotate fewer items than required for constructor", provide: fx.Provide( newFromStringer, func() anotherStringer { return anotherStringer{ "another stringer", } }, fx.Annotate( func(myStringer1 myStringer, fromStringer2 *fromStringer) fmt.Stringer { return &fromStringer{ name: myStringer1.String() + " and " + fromStringer2.String(), } }, fx.From(new(anotherStringer)), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "another stringer and a good stringer") }, }, { desc: "Provide with empty From type", provide: fx.Provide( newFromStringer, fx.Annotate( func(myStringer *fromStringer) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.From(), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "a good stringer") }, }, { desc: "Provide with variadic function", provide: fx.Provide( newFromStringer, fx.Annotate( func(myStringer myStringer, x ...int) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.From(new(*fromStringer)), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "a good stringer") }, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(t) }), tt.provide, fx.Invoke(tt.invoke), ) require.NoError(t, app.Err()) }) } } func TestAnnotatedFromFailures(t *testing.T) { t.Parallel() type myStringer interface { String() string } newFromStringer := func() *fromStringer { return &fromStringer{name: "stringer"} } tests := []struct { desc string provide fx.Option invoke any errorContains string }{ { desc: "provide when an illegal type From", provide: fx.Provide( fx.Annotate( func(writer io.Writer) fmt.Stringer { return &fromStringer{} }, fx.From(new(*fromStringer)), ), ), invoke: func(stringer fmt.Stringer) { fmt.Println(stringer.String()) }, errorContains: "*fx_test.fromStringer does not implement io.Writer", }, { desc: "provide with variadic function and an illegal type From", provide: fx.Provide( fx.Annotate( func(writer io.Writer, x ...int) fmt.Stringer { return &fromStringer{} }, fx.From(new(*fromStringer)), ), ), invoke: func(stringer fmt.Stringer) { fmt.Println(stringer.String()) }, errorContains: "*fx_test.fromStringer does not implement io.Writer", }, { desc: "don't provide original type using From", provide: fx.Provide( fx.Annotate( func(myStringer myStringer) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.From(new(*fromStringer)), ), ), invoke: func(stringer fmt.Stringer) { fmt.Println(stringer.String()) }, errorContains: "missing type: *fx_test.fromStringer", }, { desc: "fail to provide with name annotation", provide: fx.Provide( fx.Annotate( newFromStringer, ), fx.Annotate( func(myStringer myStringer) fmt.Stringer { return &fromStringer{ name: myStringer.String(), } }, fx.From(new(*fromStringer)), fx.ParamTags(`name:"struct1"`), ), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "a good stringer") }, errorContains: `missing type: *fx_test.fromStringer[name="struct1"]`, }, { desc: "non-pointer argument to From", provide: fx.Provide( fx.Annotate( newFromStringer, fx.From("foo"), ), ), errorContains: "argument must be a pointer", }, { desc: "multiple from annotations", provide: fx.Provide( fx.Annotate( newFromStringer, fx.From(new(asStringer)), fx.From(new(asStringer)), ), ), errorContains: "cannot apply more than one line of From", }, { desc: "variadic argument", provide: fx.Provide( fx.Annotate( func(ss ...myStringer) {}, fx.From(new(asStringer)), ), ), errorContains: "cannot annotate a variadic argument", }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(t) }), tt.provide, fx.Invoke(tt.invoke), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), tt.errorContains) }) } } func TestAnnotatedAs(t *testing.T) { t.Parallel() type in struct { fx.In S fmt.Stringer `name:"goodStringer"` } type inSelf struct { fx.In S1 fmt.Stringer `name:"goodStringer"` S2 *asStringer `name:"goodStringer"` } type myStringer interface { String() string } newAsStringer := func() *asStringer { return &asStringer{ name: "a good stringer", } } tests := []struct { desc string provide fx.Option invoke any startApp bool }{ { desc: "provide a good stringer", provide: fx.Provide( fx.Annotate(newAsStringer, fx.As(new(fmt.Stringer))), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "a good stringer") }, }, { desc: "value type implementing interface", provide: fx.Provide( fx.Annotate(func() anotherStringer { return anotherStringer{ "another stringer", } }, fx.As(new(fmt.Stringer))), ), invoke: func(s fmt.Stringer) { assert.Equal(t, s.String(), "another stringer") }, }, { desc: "provide with multiple types As", provide: fx.Provide(fx.Annotate(func() (*asStringer, *bytes.Buffer) { buf := make([]byte, 1) b := bytes.NewBuffer(buf) return &asStringer{name: "stringer"}, b }, fx.As(new(fmt.Stringer), new(io.Writer)))), invoke: func(s fmt.Stringer, w io.Writer) { w.Write([]byte(s.String())) }, }, { desc: "provide as with result annotation", provide: fx.Provide( fx.Annotate(func() *asStringer { return &asStringer{name: "stringer"} }, fx.ResultTags(`name:"goodStringer"`), fx.As(new(fmt.Stringer))), ), invoke: func(i in) { assert.Equal(t, "stringer", i.S.String()) }, }, { // same as the test above, except now we annotate // it in a different order. desc: "provide as with result annotation, in different order", provide: fx.Provide( fx.Annotate(func() *asStringer { return &asStringer{name: "stringer"} }, fx.As(new(fmt.Stringer)), fx.ResultTags(`name:"goodStringer"`)), ), invoke: func(i in) { assert.Equal(t, "stringer", i.S.String()) }, }, { desc: "provide as with result annotation with error", provide: fx.Provide( fx.Annotate(func() (*asStringer, error) { return &asStringer{name: "stringer"}, nil }, fx.ResultTags(`name:"goodStringer"`), fx.As(new(fmt.Stringer))), ), invoke: func(i in) { assert.Equal(t, "stringer", i.S.String()) }, }, { desc: "provide as with result annotation in different order with error", provide: fx.Provide( fx.Annotate(func() (*asStringer, error) { return &asStringer{name: "stringer"}, nil }, fx.As(new(fmt.Stringer)), fx.ResultTags(`name:"goodStringer"`)), ), invoke: func(i in) { assert.Equal(t, "stringer", i.S.String()) }, }, { desc: "provide multiple constructors annotated As", provide: fx.Provide( fx.Annotate(func() *asStringer { return &asStringer{name: "stringer"} }, fx.As(new(fmt.Stringer))), fx.Annotate(func() *bytes.Buffer { buf := make([]byte, 1) return bytes.NewBuffer(buf) }, fx.As(new(io.Writer))), ), invoke: func(s fmt.Stringer, w io.Writer) { assert.Equal(t, "stringer", s.String()) _, err := w.Write([]byte{1}) require.NoError(t, err) }, }, { desc: "provide the same provider as multiple types", provide: fx.Provide( fx.Annotate(newAsStringer, fx.As(new(fmt.Stringer))), fx.Annotate(newAsStringer, fx.As(new(myStringer))), ), invoke: func(s fmt.Stringer, ms myStringer) { assert.Equal(t, "a good stringer", s.String()) assert.Equal(t, "a good stringer", ms.String()) }, }, { desc: "annotate fx.Supply", provide: fx.Supply( fx.Annotate(&asStringer{"foo"}, fx.As(new(fmt.Stringer))), ), invoke: func(s fmt.Stringer) { assert.Equal(t, "foo", s.String()) }, }, { desc: "annotate as many interfaces", provide: fx.Provide( fx.Annotate(func() (*asStringer, error) { return &asStringer{name: "stringer"}, nil }, fx.As(new(fmt.Stringer)), fx.As(new(myStringer)), fx.ResultTags(`name:"stringer"`)), ), invoke: fx.Annotate( func( S fmt.Stringer, MS myStringer, ) { assert.Equal(t, "stringer", S.String()) assert.Equal(t, "stringer", MS.String()) }, fx.ParamTags(`name:"stringer"`, `name:"stringer"`), ), }, { desc: "annotate as many interfaces with both-annotated return values", provide: fx.Provide( fx.Annotate(func() (*asStringer, *bytes.Buffer) { return &asStringer{name: "stringer"}, bytes.NewBuffer(make([]byte, 1)) }, fx.As(new(fmt.Stringer), new(io.Reader)), fx.As(new(myStringer), new(io.Writer))), ), invoke: func(s fmt.Stringer, ms myStringer, r io.Reader, w io.Writer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", ms.String()) _, err := w.Write([]byte(".")) assert.NoError(t, err) buf := make([]byte, 1) _, err = r.Read(buf) assert.NoError(t, err) }, }, { desc: "annotate as many interfaces with different numbers of annotations", provide: fx.Provide( fx.Annotate(func() (*asStringer, *bytes.Buffer) { return &asStringer{name: "stringer"}, bytes.NewBuffer(make([]byte, 1)) }, // annotate both in here fx.As(new(fmt.Stringer), new(io.Writer)), // annotate just myStringer here fx.As(new(myStringer))), ), invoke: func(s fmt.Stringer, ms myStringer, w io.Writer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", ms.String()) _, err := w.Write([]byte(".")) assert.NoError(t, err) }, }, { desc: "annotate many interfaces with varying annotation count and check original type", provide: fx.Provide( fx.Annotate(func() (*asStringer, *bytes.Buffer) { return &asStringer{name: "stringer"}, bytes.NewBuffer(make([]byte, 1)) }, // annotate just myStringer here fx.As(new(myStringer)), // annotate both in here fx.As(new(fmt.Stringer), new(io.Writer))), ), invoke: func(s fmt.Stringer, ms myStringer, buf *bytes.Buffer, w io.Writer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", ms.String()) _, err := w.Write([]byte(".")) assert.NoError(t, err) _, err = buf.Write([]byte(".")) assert.NoError(t, err) }, }, { desc: "annotate fewer items than provided constructor", provide: fx.Provide( fx.Annotate(func() (*bytes.Buffer, *strings.Builder) { s := "Hello" return bytes.NewBuffer([]byte(s)), &strings.Builder{} }, fx.As(new(io.Reader))), ), invoke: func(r io.Reader) { }, }, { desc: "results annotated as are provided to hooks as annotated types", provide: fx.Provide( fx.Annotate(func() (*asStringer, *bytes.Buffer) { return &asStringer{name: "stringer"}, bytes.NewBuffer([]byte{}) }, // lifecycle hook added is able to receive results as annotated fx.OnStart(func(s fmt.Stringer, ms myStringer, buf *bytes.Buffer, w io.Writer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", ms.String()) _, err := w.Write([]byte(".")) assert.NoError(t, err) _, err = buf.Write([]byte(".")) assert.NoError(t, err) }), fx.OnStop(func(buf *bytes.Buffer) { assert.Equal(t, "....", buf.String(), "buffer should contain bytes written in Invoke func and OnStart hook") }), // annotate just myStringer here fx.As(new(myStringer)), // annotate both in here fx.As(new(fmt.Stringer), new(io.Writer))), ), invoke: func(s fmt.Stringer, ms myStringer, buf *bytes.Buffer, w io.Writer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", ms.String()) _, err := w.Write([]byte(".")) assert.NoError(t, err) _, err = buf.Write([]byte(".")) assert.NoError(t, err) }, startApp: true, }, { desc: "self w other As annotations", provide: fx.Provide( fx.Annotate( func() *asStringer { return &asStringer{name: "stringer"} }, fx.As(fx.Self()), fx.As(new(fmt.Stringer)), ), ), invoke: func(s fmt.Stringer, as *asStringer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", as.String()) }, }, { desc: "self as one As target", provide: fx.Provide( fx.Annotate( func() (*asStringer, *bytes.Buffer) { s := &asStringer{name: "stringer"} b := &bytes.Buffer{} return s, b }, fx.As(fx.Self(), new(io.Writer)), ), ), invoke: func(s *asStringer, w io.Writer) { assert.Equal(t, "stringer", s.String()) _, err := w.Write([]byte(".")) assert.NoError(t, err) }, }, { desc: "two as, two self, four types", provide: fx.Provide( fx.Annotate( func() (*asStringer, *bytes.Buffer) { s := &asStringer{name: "stringer"} b := &bytes.Buffer{} return s, b }, fx.As(fx.Self(), new(io.Writer)), fx.As(new(fmt.Stringer)), ), ), invoke: func(s1 *asStringer, s2 fmt.Stringer, b *bytes.Buffer, w io.Writer) { assert.Equal(t, "stringer", s1.String()) assert.Equal(t, "stringer", s2.String()) _, err := w.Write([]byte(".")) assert.NoError(t, err) _, err = b.Write([]byte(".")) assert.NoError(t, err) }, }, { desc: "self with lifecycle hook", provide: fx.Provide( fx.Annotate( func() *asStringer { return &asStringer{name: "stringer"} }, fx.As(fx.Self()), fx.As(new(fmt.Stringer)), fx.OnStart(func(s fmt.Stringer, as *asStringer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", as.String()) }), ), ), invoke: func(s fmt.Stringer, as *asStringer) { assert.Equal(t, "stringer", s.String()) assert.Equal(t, "stringer", as.String()) }, startApp: true, }, { desc: "self with result tags", provide: fx.Provide( fx.Annotate( func() *asStringer { return &asStringer{name: "stringer"} }, fx.As(fx.Self()), fx.As(new(fmt.Stringer)), fx.ResultTags(`name:"goodStringer"`), ), ), invoke: func(i inSelf) { assert.Equal(t, "stringer", i.S1.String()) assert.Equal(t, "stringer", i.S2.String()) }, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(t) }), tt.provide, fx.Invoke(tt.invoke), ) require.NoError(t, app.Err()) if tt.startApp { ctx := context.Background() require.NoError(t, app.Start(ctx)) require.NoError(t, app.Stop(ctx)) } }) } } func TestAnnotatedAsFailures(t *testing.T) { t.Parallel() newAsStringer := func() *asStringer { return &asStringer{name: "stringer"} } newAsStringerWithErr := func() (*asStringer, error) { return nil, errors.New("great sadness") } tests := []struct { desc string provide fx.Option invoke any errorContains string }{ { desc: "provide when an illegal type As", provide: fx.Provide(fx.Annotate(newAsStringer, fx.As(new(io.Writer)))), invoke: func() {}, errorContains: "asStringer does not implement io.Writer", }, { desc: "provide when an illegal type As with result tag", provide: fx.Provide(fx.Annotate(newAsStringer, fx.ResultTags(`name:"stringer"`), fx.As(new(io.Writer)))), invoke: func() {}, errorContains: "asStringer does not implement io.Writer", }, { desc: "error is propagated without result tag", provide: fx.Provide(fx.Annotate(newAsStringerWithErr, fx.As(new(fmt.Stringer)))), invoke: func(_ fmt.Stringer) {}, errorContains: "great sadness", }, { desc: "error is propagated with result tag", provide: fx.Provide(fx.Annotate(newAsStringerWithErr, fx.ResultTags(`name:"stringer"`), fx.As(new(fmt.Stringer)))), invoke: fx.Annotate(func(_ fmt.Stringer) {}, fx.ParamTags(`name:"stringer"`)), errorContains: "great sadness", }, { desc: "don't provide original type using As", provide: fx.Provide(fx.Annotate(newAsStringer, fx.As(new(fmt.Stringer)))), invoke: func(as *asStringer) { fmt.Println(as.String()) }, errorContains: "missing type: *fx_test.asStringer", }, { desc: "fail to provide with name annotation", provide: fx.Provide(fx.Annotate(func(n string) *asStringer { return &asStringer{name: n} }, fx.As(new(fmt.Stringer)), fx.ParamTags(`name:"n"`))), invoke: func(a fmt.Stringer) { fmt.Println(a) }, errorContains: `missing type: string[name="n"]`, }, { desc: "non-pointer argument to As", provide: fx.Provide( fx.Annotate( newAsStringer, fx.As("foo"), ), ), errorContains: "argument must be a pointer to an interface: got string", }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(t) }), tt.provide, fx.Invoke(tt.invoke), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), tt.errorContains) }) } } func TestAnnotatedWrongUsage(t *testing.T) { t.Parallel() type a struct { name string } type in struct { fx.In A *a `name:"foo"` } newA := func() *a { return &a{name: "foo"} } t.Run("In Constructor", func(t *testing.T) { t.Parallel() var in in app := NewForTest(t, fx.WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(t) }), fx.Provide( func() fx.Annotated { return fx.Annotated{ Name: "foo", Target: newA, } }, ), fx.Populate(&in), ) err := app.Err() require.Error(t, err) // Example: // fx.Annotated should be passed to fx.Provide directly, it should not be returned by the constructor: fx.Provide received go.uber.org/fx_test.TestAnnotatedWrongUsage.func2.1() from: // go.uber.org/fx_test.TestAnnotatedWrongUsage.func2 // /.../fx/annotated_test.go:76 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 assert.Contains(t, err.Error(), "fx.Annotated should be passed to fx.Provide directly, it should not be returned by the constructor") assert.Contains(t, err.Error(), "fx.Provide received go.uber.org/fx_test.TestAnnotatedWrongUsage") assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestAnnotatedWrongUsage") assert.Contains(t, err.Error(), "/annotated_test.go") }) t.Run("Result Type", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(t) }), fx.Provide( fx.Annotated{ Name: "foo", Target: func() in { return in{A: &a{name: "foo"}} }, }, ), ) assert.Contains(t, app.Err().Error(), "embeds a dig.In", "expected error when result types were annotated") }) t.Run("invalid group option", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( fx.Annotate(func() string { return "sad times" }, fx.ResultTags(`group:"foo,soft"`)), ), ) assert.Contains(t, app.Err().Error(), "cannot use soft with result value groups", "expected error when invalid group option is provided") }) } func TestAnnotatedString(t *testing.T) { t.Parallel() tests := []struct { desc string give fx.Annotated want string }{ { desc: "empty", give: fx.Annotated{}, want: "fx.Annotated{}", }, { desc: "name", give: fx.Annotated{Name: "foo"}, want: `fx.Annotated{Name: "foo"}`, }, { desc: "group", give: fx.Annotated{Group: "foo"}, want: `fx.Annotated{Group: "foo"}`, }, { desc: "name and group", give: fx.Annotated{Name: "foo", Group: "bar"}, want: `fx.Annotated{Name: "foo", Group: "bar"}`, }, { desc: "target", give: fx.Annotated{Target: func() {}}, want: "fx.Annotated{Target: go.uber.org/fx_test.TestAnnotatedString.func1()}", }, { desc: "name and target", give: fx.Annotated{Name: "foo", Target: func() {}}, want: `fx.Annotated{Name: "foo", Target: go.uber.org/fx_test.TestAnnotatedString.func2()}`, }, { desc: "group and target", give: fx.Annotated{Group: "foo", Target: func() {}}, want: `fx.Annotated{Group: "foo", Target: go.uber.org/fx_test.TestAnnotatedString.func3()}`, }, { desc: "name, group and target", give: fx.Annotated{Name: "foo", Group: "bar", Target: func() {}}, want: `fx.Annotated{Name: "foo", Group: "bar", Target: go.uber.org/fx_test.TestAnnotatedString.func4()}`, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() assert.Equal(t, tt.want, tt.give.String()) }) } } func TestAnnotate(t *testing.T) { t.Parallel() type a struct{} type b struct{ a *a } type c struct{ b *b } type sliceA struct{ sa []*a } newA := func() *a { return &a{} } newB := func(a *a) *b { return &b{a} } newC := func(b *b) *c { return &c{b} } newSliceA := func(sa ...*a) *sliceA { return &sliceA{sa} } newSliceAWithB := func(b *b, sa ...*a) *sliceA { total := append(sa, b.a) return &sliceA{total} } t.Run("Provide with empty param+result tags", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Provide( newA, fx.Annotate(newB, fx.ParamTags(), fx.ResultTags()), ), fx.Invoke(newC), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("Provide with optional", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Provide( fx.Annotate(newB, fx.ParamTags(`name:"a" optional:"true"`)), ), fx.Invoke(newC), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("Provide with many annotated params", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Provide( fx.Annotate(newB, fx.ParamTags(`optional:"true"`)), fx.Annotate(func(a *a, b *b) any { return nil }, fx.ParamTags(`name:"a" optional:"true"`, `name:"b"`), fx.ResultTags(`name:"nil"`), ), ), fx.Invoke(newC), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("Invoke with optional", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Invoke( fx.Annotate(newB, fx.ParamTags(`optional:"true"`)), ), ) err := app.Err() require.NoError(t, err) }) t.Run("Invoke with a missing dependency", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Invoke( fx.Annotate(newB, fx.ParamTags(`name:"a"`)), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), `missing dependencies`) assert.Contains(t, err.Error(), `missing type: *fx_test.a[name="a"]`) }) t.Run("Provide with variadic function", func(t *testing.T) { t.Parallel() var got *sliceA app := fxtest.New(t, fx.Provide( fx.Annotated{Group: "as", Target: newA}, fx.Annotated{Group: "as", Target: newA}, fx.Annotate(newSliceA, fx.ParamTags(`group:"as"`), ), ), fx.Populate(&got), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) assert.Len(t, got.sa, 2) }) t.Run("Provide variadic function with no optional params", func(t *testing.T) { t.Parallel() var got struct { fx.In Result *sliceA `name:"as"` } app := fxtest.New(t, fx.Supply([]*a{{}, {}, {}}), fx.Provide( fx.Annotate(newSliceA, fx.ResultTags(`name:"as"`), ), ), fx.Populate(&got), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) assert.Len(t, got.Result.sa, 3) }) t.Run("Provide variadic function named with no given params", func(t *testing.T) { t.Parallel() var got *sliceA app := NewForTest(t, fx.Provide( fx.Annotate(newSliceA, fx.ParamTags(`name:"a"`)), ), fx.Populate(&got), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), `missing dependencies`) assert.Contains(t, err.Error(), `missing type: []*fx_test.a[name="a"]`) }) t.Run("Invoke function with soft group param", func(t *testing.T) { t.Parallel() newF := func(foos []int, bar string) { assert.ElementsMatch(t, []int{10}, foos) } app := fxtest.New(t, fx.Provide( fx.Annotate( func() (int, string) { return 10, "hello" }, fx.ResultTags(`group:"foos"`), ), fx.Annotate( func() int { require.FailNow(t, "this function should not be called") return 20 }, fx.ResultTags(`group:"foos"`), ), ), fx.Invoke( fx.Annotate(newF, fx.ParamTags(`group:"foos,soft"`)), ), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("Invoke variadic function with multiple params", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Supply( fx.Annotate(newB(newA()), fx.ResultTags(`name:"b"`)), ), fx.Invoke(fx.Annotate(newSliceAWithB, fx.ParamTags(`name:"b"`))), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("Invoke non-optional variadic function with a missing dependency", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Invoke( fx.Annotate(newSliceA, fx.ParamTags(`optional:"false"`)), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), `missing dependencies`) assert.Contains(t, err.Error(), `missing type: []*fx_test.a`) }) t.Run("Invoke with variadic function", func(t *testing.T) { t.Parallel() type T1 struct{ s string } app := fxtest.New(t, fx.Supply( fx.Annotate(T1{"foo"}, fx.ResultTags(`group:"t"`)), fx.Annotate(T1{"bar"}, fx.ResultTags(`group:"t"`)), fx.Annotate(T1{"baz"}, fx.ResultTags(`group:"t"`)), ), fx.Invoke(fx.Annotate(func(got ...T1) { assert.ElementsMatch(t, []T1{{"foo"}, {"bar"}, {"baz"}}, got) }, fx.ParamTags(`group:"t"`))), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("provide with annotated results", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Provide( fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"firstA"`)), fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"secondA"`)), fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"thirdA"`)), fx.Annotate(func(a1 *a, a2 *a, a3 *a) *b { return &b{a1} }, fx.ParamTags( `name:"firstA"`, `name:"secondA"`, `name:"thirdA"`, )), ), fx.Invoke(newC), ) require.NoError(t, app.Err()) defer app.RequireStart().RequireStop() }) t.Run("provide with missing annotated results", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"firstA"`)), fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"secondA"`)), fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"fourthA"`)), ), fx.Invoke( fx.Annotate(func(a1 *a, a2 *a, a3 *a) *b { return &b{a1} }, fx.ParamTags( `name:"firstA"`, `name:"secondA"`, `name:"thirdA"`)), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), `missing type: *fx_test.a[name="thirdA"]`) }) t.Run("error in the middle of a function", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( fx.Annotate(func() (*a, error, *a) { //nolint:staticcheck // we want to test error in the middle. return &a{}, nil, &a{} }, fx.ResultTags(`name:"firstA"`, ``, `name:"secondA"`)), ), fx.Invoke( fx.Annotate(func(*a) {}, fx.ParamTags(`name:"firstA"`)), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "only the last result can be an error") assert.Contains(t, err.Error(), "returns error as result 1") }) t.Run("provide with annotated results with error", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Provide( fx.Annotate(func() (*a, *a, error) { return &a{}, &a{}, nil }, fx.ResultTags(`name:"firstA"`, `name:"secondA"`)), fx.Annotate(func() (*a, error) { return &a{}, nil }, fx.ResultTags(`name:"thirdA"`)), ), fx.Invoke(fx.Annotate(func(a1 *a, a2 *a, a3 *a) *b { return &b{a2} }, fx.ParamTags(`name:"firstA"`, `name:"secondA"`, `name:"thirdA"`)))) require.NoError(t, app.Err()) defer app.RequireStart().RequireStop() }) t.Run("provide an already provided function using Annotate", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide(fx.Annotate(newA, fx.ResultTags(`name:"a"`))), fx.Provide(fx.Annotate(newA, fx.ResultTags(`name:"a"`))), fx.Invoke( fx.Annotate(newB, fx.ParamTags(`name:"a"`)), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "already provided") assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestAnnotate.func") }) t.Run("specify more ParamTags than Params", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Provide( // This should just leave newA as it is. fx.Annotate(newA, fx.ParamTags(`name:"something"`)), ), fx.Invoke(newB), ) err := app.Err() require.NoError(t, err) defer app.RequireStart().RequireStop() }) t.Run("specify two ParamTags", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( // This should just leave newA as it is. fx.Annotate( newA, fx.ParamTags(`name:"something"`), fx.ParamTags(`name:"anotherThing"`), ), ), fx.Invoke(newB), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "encountered error while applying annotation using fx.Annotate to go.uber.org/fx_test.TestAnnotate.func1(): cannot apply more than one line of ParamTags") }) t.Run("specify two ResultTags", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( // This should just leave newA as it is. fx.Annotate( newA, fx.ResultTags(`name:"A"`), fx.ResultTags(`name:"AA"`), ), ), fx.Invoke(newB), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "encountered error while applying annotation using fx.Annotate to go.uber.org/fx_test.TestAnnotate.func1(): cannot apply more than one line of ResultTags") }) t.Run("annotate with a non-nil error", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( fx.Annotate(func() (*bytes.Buffer, error) { buf := make([]byte, 1) return bytes.NewBuffer(buf), errors.New("some error") }, fx.ResultTags(`name:"buf"`))), fx.Invoke( fx.Annotate(func(b *bytes.Buffer) { b.Write([]byte{1}) }, fx.ParamTags(`name:"buf"`))), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "some error") }) t.Run("annotate with a non-nil error and nil error", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( fx.Annotate(func() (*bytes.Buffer, error) { buf := make([]byte, 1) return bytes.NewBuffer(buf), errors.New("some error") }, fx.ResultTags(`name:"buf1"`)), fx.Annotate(func() (*bytes.Buffer, error) { buf := make([]byte, 1) return bytes.NewBuffer(buf), nil }, fx.ResultTags(`name:"buf2"`))), fx.Invoke( fx.Annotate(func(b1 *bytes.Buffer, b2 *bytes.Buffer) { b1.Write([]byte{1}) b2.Write([]byte{1}) }, fx.ParamTags(`name:"buf1"`, `name:"buf2"`))), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "some error") }) t.Run("provide annotated non-function", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Provide( fx.Annotate(42, fx.ResultTags(`name:"buf"`)), ), ) err := app.Err() require.Error(t, err) // Example: // fx.Provide(fx.Annotate(42, fx.ResultTags(["name:\"buf\""])) from: // go.uber.org/fx_test.TestAnnotate.func17 // /Users/abg/dev/fx/annotated_test.go:697 // testing.tRunner // /usr/local/Cellar/go/1.17.2/libexec/src/testing/testing.go:1259 // Failed: must provide constructor function, got 42 (int) assert.Contains(t, err.Error(), "fx.Provide(fx.Annotate(42") assert.Contains(t, err.Error(), "must provide constructor function, got 42 (int)") }) t.Run("invoke annotated non-function", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Invoke( fx.Annotate(42, fx.ParamTags(`name:"buf"`)), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "must provide constructor function, got 42 (int)") }) t.Run("annotate a fx.Out with ResultTags", func(t *testing.T) { t.Parallel() type A struct { s string fx.Out } f := func() A { return A{s: "hi"} } app := NewForTest(t, fx.Provide( fx.Annotate(f, fx.ResultTags(`name:"out"`)), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "fx.Out structs cannot be annotated with fx.ResultTags or fx.As") }) t.Run("annotate a fx.Out with As", func(t *testing.T) { t.Parallel() type I any type B struct { // implements I } type Res struct { fx.Out AB B } f := func() Res { return Res{AB: B{}} } app := NewForTest(t, fx.Provide( fx.Annotate(f, fx.As(new(I))), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "fx.Out structs cannot be annotated with fx.ResultTags or fx.As") }) t.Run("annotate a fx.In with ParamTags", func(t *testing.T) { t.Parallel() type A struct { S string } type B struct { fx.In } app := NewForTest(t, fx.Provide( fx.Annotate(func(i A) string { return i.S }, fx.ParamTags(`optional:"true"`)), fx.Annotate(func(i B) string { return "ok" }, fx.ParamTags(`name:"problem"`)), ), ) err := app.Err() require.Error(t, err) assert.NotContains(t, err.Error(), "invalid annotation function func(fx_test.A) string") assert.Contains(t, err.Error(), "invalid annotation function func(fx_test.B) string") assert.Contains(t, err.Error(), "fx.In structs cannot be annotated with fx.ParamTags or fx.From") }) t.Run("annotate a fx.In with From", func(t *testing.T) { t.Parallel() type I any type B struct { // implements I } type Param struct { fx.In BInterface I } app := NewForTest(t, fx.Provide( fx.Annotate(func(p Param) string { return "ok" }, fx.From(new(B))), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "invalid annotation function func(fx_test.Param) string") assert.Contains(t, err.Error(), "fx.In structs cannot be annotated with fx.ParamTags or fx.From") }) t.Run("annotate fx.In with fx.ResultTags", func(t *testing.T) { t.Parallel() type A struct { fx.In I int } app := NewForTest(t, fx.Provide( fx.Annotate(func(a A) string { return "ok" + strconv.Itoa(a.I) }, fx.ResultTags(`name:"val"`)), func() int { return 1 }, ), fx.Invoke( fx.Annotate(func(s string) { assert.Equal(t, "ok1", s) }, fx.ParamTags(`name:"val"`)), ), ) err := app.Err() require.NoError(t, err) }) t.Run("annotate fx.Out with fx.ParamTags", func(t *testing.T) { t.Parallel() type A struct { fx.Out S string } app := NewForTest(t, fx.Provide( fx.Annotate(func() int { return 1 }, fx.ResultTags(`name:"val"`)), fx.Annotate(func(i int) A { return A{S: strconv.Itoa(i)} }, fx.ParamTags(`name:"val"`)), ), fx.Invoke(func(s string) { assert.Equal(t, "1", s) }), ) err := app.Err() require.NoError(t, err) }) } func TestAnnotateApplyFail(t *testing.T) { type a struct{} type b struct{ a *a } newA := func() *a { return &a{} } newB := func(a *a) *b { return &b{a} } var ( errTagSyntaxSpace = `multiple tags are not separated by space` errTagKeySyntax = "tag key is invalid, Use group, name or optional as tag keys" errTagValueSyntaxQuote = `tag value should start with double quote. i.e. key:"value" ` errTagValueSyntaxEndingQuote = `tag value should end in double quote. i.e. key:"value" ` ) tests := []struct { give string wantErr string giveAnnotationParam fx.Annotation giveAnnotationResult fx.Annotation }{ { give: "Tags value invalid ending quote", wantErr: errTagValueSyntaxEndingQuote, giveAnnotationParam: fx.ParamTags(`name:"something'`), giveAnnotationResult: fx.ResultTags(`name:"something'`), }, { give: "Tags value wrong starting quote", wantErr: errTagValueSyntaxQuote, giveAnnotationParam: fx.ParamTags(`name:"something" optional:'true"`), giveAnnotationResult: fx.ResultTags(`name:"something" optional:'true"`), }, { give: "Tags multiple tags not separated by space", wantErr: errTagSyntaxSpace, giveAnnotationParam: fx.ParamTags(`name:"something"group:"something"`), giveAnnotationResult: fx.ResultTags(`name:"something"group:"something"`), }, { give: "Tags key not equal to group, name or optional", wantErr: errTagKeySyntax, giveAnnotationParam: fx.ParamTags(`name1:"something"`), giveAnnotationResult: fx.ResultTags(`name1:"something"`), }, { give: "Tags key empty", wantErr: errTagKeySyntax, giveAnnotationParam: fx.ParamTags(`:"something"`), giveAnnotationResult: fx.ResultTags(`:"something"`), }, } for _, tt := range tests { t.Run("Param "+tt.give, func(t *testing.T) { app := NewForTest(t, fx.Provide( fx.Annotate( newA, tt.giveAnnotationParam, ), ), fx.Invoke(newB), ) assert.ErrorContains(t, app.Err(), tt.wantErr) }) t.Run("Result "+tt.give, func(t *testing.T) { app := NewForTest(t, fx.Provide( fx.Annotate( newA, tt.giveAnnotationResult, ), ), fx.Invoke(newB), ) assert.ErrorContains(t, app.Err(), tt.wantErr) }) } } func TestAnnotateApplySuccess(t *testing.T) { type a struct{} type b struct{ a *a } newA := func() *a { return &a{} } newB := func(a *a) *b { return &b{a} } tests := []struct { give string giveAnnotationParam fx.Annotation giveAnnotationResult fx.Annotation }{ { give: "ParamTags Tag Empty", giveAnnotationParam: fx.ParamTags(` `), giveAnnotationResult: fx.ResultTags(` `), }, { give: "ParamTags Tag Empty with extra spaces", giveAnnotationParam: fx.ParamTags(`name:"versionNum"`, ` `), giveAnnotationResult: fx.ResultTags(` `, `group:"versionNum"`), }, { give: "ParamTags Tag with \\ ", giveAnnotationParam: fx.ParamTags(`name:"version\\Num"`, ` `), giveAnnotationResult: fx.ResultTags(``, `group:"version\\Num"`), }, } for _, tt := range tests { t.Run(tt.give, func(t *testing.T) { app := NewForTest(t, fx.Provide( fx.Annotate( newA, tt.giveAnnotationParam, tt.giveAnnotationResult, ), ), fx.Invoke(newB), ) require.NoError(t, app.Err()) }) } } func assertApp( t *testing.T, app interface { Start(context.Context) error Stop(context.Context) error }, started *bool, stopped *bool, invoked *bool, ) { t.Helper() ctx, cancel := context.WithCancel(context.Background()) defer cancel() assert.False(t, *started) require.NoError(t, app.Start(ctx)) assert.True(t, *started) if invoked != nil { assert.True(t, *invoked) } if stopped != nil { assert.False(t, *stopped) require.NoError(t, app.Stop(ctx)) assert.True(t, *stopped) } defer app.Stop(ctx) } func TestHookAnnotations(t *testing.T) { t.Parallel() type a struct{} type b struct{ a *a } type c struct{ b *b } newB := func(a *a) *b { return &b{a} } newC := func(b *b) *c { return &c{b} } t.Run("with hook on invoke", func(t *testing.T) { t.Parallel() var ( started bool stopped bool invoked bool ) hook := fx.Annotate( func() { invoked = true }, fx.OnStart(func(context.Context) error { started = true return nil }), fx.OnStop(func(context.Context) error { stopped = true return nil }), ) app := fxtest.New(t, fx.Invoke(hook)) assertApp(t, app, &started, &stopped, &invoked) }) t.Run("depend on result interface of target", func(t *testing.T) { type stub interface { String() string } var started bool hook := fx.Annotate( func() (stub, error) { b := []byte("expected") return bytes.NewBuffer(b), nil }, fx.OnStart(func(_ context.Context, s stub) error { started = true require.Equal(t, "expected", s.String()) return nil }), ) app := fxtest.New(t, fx.Provide(hook), fx.Invoke(func(s stub) { require.Equal(t, "expected", s.String()) }), ) assertApp(t, app, &started, nil, nil) }) t.Run("start and stop without dependencies", func(t *testing.T) { t.Parallel() type stub any var ( invoked bool started bool stopped bool ) hook := fx.Annotate( func() (stub, error) { return nil, nil }, fx.OnStart(func(context.Context) error { started = true return nil }), fx.OnStop(func(context.Context) error { stopped = true return nil }), ) app := fxtest.New(t, fx.Provide(hook), fx.Invoke(func(s stub) { invoked = s == nil }), ) assertApp(t, app, &started, &stopped, &invoked) }) t.Run("with multiple extra dependency parameters", func(t *testing.T) { t.Parallel() type ( A any B any C any ) var value int hook := fx.Annotate( func(b B, c C) (A, error) { return nil, nil }, fx.OnStart(func(_ context.Context, b B, c C) error { b1, _ := b.(int) c1, _ := c.(int) value = b1 + c1 return nil }), ) app := fxtest.New(t, fx.Provide(hook), fx.Provide(func() B { return int(1) }), fx.Provide(func() C { return int(2) }), fx.Invoke(func(A) {}), ) ctx := context.Background() assert.Zero(t, value) require.NoError(t, app.Start(ctx)) defer func() { require.NoError(t, app.Stop(ctx)) }() assert.Equal(t, 3, value) }) t.Run("with Supply", func(t *testing.T) { t.Parallel() type A interface { WriteString(string) (int, error) } buf := bytes.NewBuffer(nil) var called bool ctor := fx.Provide( fx.Annotate( func(s fmt.Stringer) A { return buf }, fx.OnStart(func(_ context.Context, a A, s fmt.Stringer) error { a.WriteString(s.String()) return nil }), ), ) supply := fx.Supply( fx.Annotate( &asStringer{"supply"}, fx.OnStart(func(context.Context) error { called = true return nil }), fx.As(new(fmt.Stringer)), )) opts := fx.Options( ctor, supply, fx.Invoke(func(A) {}), ) app := fxtest.New(t, opts) ctx := context.Background() require.False(t, called) err := app.Start(ctx) require.NoError(t, err) require.NoError(t, app.Stop(ctx)) require.Equal(t, "supply", buf.String()) require.True(t, called) }) t.Run("with Decorate", func(t *testing.T) { t.Parallel() type A interface { WriteString(string) (int, error) } buf := bytes.NewBuffer(nil) ctor := fx.Provide(func() A { return buf }) var called bool hook := fx.Annotate( func(in A) A { in.WriteString("decorated") return in }, fx.OnStart(func(_ context.Context, _ A) error { called = assert.Equal(t, "decorated", buf.String()) return nil }), ) decorated := fx.Decorate(hook) opts := fx.Options( ctor, decorated, fx.Invoke(func(A) {}), ) app := fxtest.New(t, opts) ctx := context.Background() require.NoError(t, app.Start(ctx)) require.NoError(t, app.Stop(ctx)) require.True(t, called) require.Equal(t, "decorated", buf.String()) }) t.Run("with Decorate and tags", func(t *testing.T) { t.Parallel() type A interface { WriteString(string) (int, error) } buf := bytes.NewBuffer(nil) ctor := fx.Provide( fx.Annotate( func() A { return buf }, fx.ResultTags(`name:"name"`), ), ) var called bool type hookParam struct { fx.In A A `name:"name"` } hook := fx.Annotate( func(in A) A { in.WriteString("decorated") return in }, fx.ParamTags(`name:"name"`), fx.ResultTags(`name:"name"`), fx.OnStart(func(_ context.Context, _ hookParam) error { called = assert.Equal(t, "decorated", buf.String()) return nil }), ) decorated := fx.Decorate(hook) opts := fx.Options( ctor, decorated, fx.Invoke(fx.Annotate(func(A) {}, fx.ParamTags(`name:"name"`))), ) app := fxtest.New(t, opts) ctx := context.Background() require.NoError(t, app.Start(ctx)) require.NoError(t, app.Stop(ctx)) require.True(t, called) require.Equal(t, "decorated", buf.String()) }) t.Run("with Supply and Decorate", func(t *testing.T) { t.Parallel() type A any ch := make(chan string, 3) hook := fx.Annotate( func(s fmt.Stringer) A { return nil }, fx.OnStart(func(_ context.Context, s fmt.Stringer) error { ch <- "constructor" require.Equal(t, "supply", s.String()) return nil }), ) ctor := fx.Provide(hook) hook = fx.Annotate( &asStringer{"supply"}, fx.OnStart(func(_ context.Context) error { ch <- "supply" return nil }), fx.As(new(fmt.Stringer)), ) supply := fx.Supply(hook) hook = fx.Annotate( func(in A) A { return in }, fx.OnStart(func(_ context.Context) error { ch <- "decorated" return nil }), ) decorated := fx.Decorate(hook) opts := fx.Options( ctor, supply, decorated, fx.Invoke(func(A) {}), ) app := fxtest.New(t, opts) ctx := context.Background() err := app.Start(ctx) require.NoError(t, err) require.NoError(t, app.Stop(ctx)) close(ch) require.Equal(t, "supply", <-ch) require.Equal(t, "constructor", <-ch) require.Equal(t, "decorated", <-ch) }) t.Run("Annotated params work with lifecycle hook annotations", func(t *testing.T) { t.Parallel() type paramStruct struct { fx.In A *a `name:"a" optional:"true"` B *b `name:"b"` } app := fxtest.New(t, fx.Provide( fx.Annotate(newB, fx.ParamTags(`optional:"true"`)), fx.Annotate(func(a *a, b *b) any { return nil }, fx.ParamTags(`name:"a" optional:"true"`, `name:"b"`), fx.ResultTags(`name:"nil"`), fx.OnStart(func(_ paramStruct) error { return nil }), fx.OnStop(func(_ paramStruct) error { return nil }), ), ), fx.Invoke(newC), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("provide with annotated results and lifecycle hook appended", func(t *testing.T) { t.Parallel() type firstAHookParam struct { fx.In Ctx context.Context A *a `name:"firstA"` } type secondAHookParam struct { fx.In A *a `name:"secondA"` Ctx context.Context } type thirdAHookParam struct { fx.In Ctx context.Context A *a `name:"thirdA"` } app := fxtest.New(t, fx.Provide( fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"firstA"`), fx.OnStart(func(param firstAHookParam) error { require.NotNil(t, param.Ctx, "context should be given") return nil })), fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"secondA"`), fx.OnStart(func(param secondAHookParam) error { require.NotNil(t, param.Ctx, "context not correctly injected") return nil })), fx.Annotate(func() *a { return &a{} }, fx.ResultTags(`name:"thirdA"`), fx.OnStart(func(param thirdAHookParam) error { require.NotNil(t, param.Ctx, "context not correctly injected") return nil })), fx.Annotate(func(a1 *a, a2 *a, a3 *a) *b { return &b{a1} }, fx.ParamTags( `name:"firstA"`, `name:"secondA"`, `name:"thirdA"`, )), ), fx.Invoke(newC), ) require.NoError(t, app.Err()) defer app.RequireStart().RequireStop() }) t.Run("provide with optional params and lifecycle hook", func(t *testing.T) { type taggedHookParam struct { fx.In Ctx context.Context A *a `optional:"true"` } t.Parallel() app := fxtest.New(t, fx.Provide( fx.Annotate( newB, fx.ParamTags(`optional:"true"`), fx.OnStart(func(tp taggedHookParam, B *b) { fmt.Println(tp.A) require.NotNil(t, tp.Ctx, "context not correctly injected") }), ), ), fx.Invoke(newC), ) require.NoError(t, app.Err()) defer app.RequireStart().RequireStop() }) } func TestHookAnnotationFailures(t *testing.T) { t.Parallel() validateApp := func(t *testing.T, opts ...fx.Option) error { return fx.ValidateApp( append(opts, fx.Logger(fxtest.NewTestPrinter(t)))..., ) } type ( A any B any ) type namedAndGroupHookParams struct { fx.In Ctx context.Context A *A `name:"a" group:"groupA"` } type namedHookParam struct { fx.In Ctx context.Context A *A `name:"a"` } type groupedHookParam struct { fx.In Ctx context.Context A *A `group:"groupA"` } table := []struct { name string annotation any extraOpts fx.Option useNew bool errContains string }{ { name: "with unprovided dependency", errContains: "hook function takes in a parameter of", useNew: true, annotation: fx.Annotate( func() A { return nil }, fx.OnStart(func(context.Context, B) error { return nil }), ), }, { name: "with hook that errors", errContains: "hook failed", useNew: true, annotation: fx.Annotate( func() (A, error) { return nil, nil }, fx.OnStart(func(context.Context) error { return errors.New("hook failed") }), ), }, { name: "with multiple hooks of the same type", errContains: `cannot apply more than one "OnStart" hook annotation`, annotation: fx.Annotate( func() A { return nil }, fx.OnStart(func(context.Context) error { return nil }), fx.OnStart(func(context.Context) error { return nil }), ), }, { name: "with constructor that errors", errContains: "hooks should not be installed", useNew: true, annotation: fx.Annotate( func() (A, error) { return nil, errors.New("hooks should not be installed") }, fx.OnStart(func(context.Context) error { require.FailNow(t, "hook should not be called") return nil }), ), }, { name: "without a function target", errContains: "must provide function", annotation: fx.Annotate( func() A { return nil }, fx.OnStart(&struct{}{}), ), }, { name: "invalid return: non-error return", errContains: "optional hook return may only be an error", annotation: fx.Annotate( func() A { return nil }, fx.OnStart(func(context.Context) any { return nil }), ), }, { name: "invalid return: too many returns", errContains: "optional hook return may only be an error", annotation: fx.Annotate( func() A { return nil }, fx.OnStart(func(context.Context) (any, any) { return nil, nil }), ), }, { name: "with variactic hook", errContains: "must not accept variadic", annotation: fx.Annotate( func() A { return nil }, fx.OnStart(func(context.Context, ...A) error { return nil }), ), }, { name: "with nil hook target", errContains: "cannot use nil function", annotation: fx.Annotate( func() A { return nil }, fx.OnStop(nil), ), }, { name: "cannot pull in any extra dependency other than params or results of the annotated function", errContains: "hook function takes in a parameter of \"fx_test.B\"", useNew: true, annotation: fx.Annotate( func(s string) A { return nil }, fx.OnStart(func(b B) error { return nil }), ), extraOpts: fx.Options( fx.Provide(func() string { return "test" }), fx.Provide(func() B { return nil }), ), }, { name: "cannot pull in a dependency when it's not properly named", errContains: "hook function takes in a parameter of \"*fx_test.A `name:\"a\"`\"", useNew: true, annotation: fx.Annotate( func(s A) A { return nil }, fx.OnStart(func(b namedHookParam) error { return nil }), ), }, { name: "cannot pull in a dependency when it's not properly grouped", errContains: "hook function takes in a parameter of \"*fx_test.A `group:\"groupA\"`", useNew: true, annotation: fx.Annotate( func(s A) A { return nil }, fx.OnStart(func(b groupedHookParam) error { return nil }), ), }, { name: "cannot pull in a dependency when it's not properly named and grouped", errContains: "hook function takes in a parameter of \"*fx_test.A `name:\"a\" group:\"groupA\"`", useNew: true, annotation: fx.Annotate( func(s A) A { return nil }, fx.OnStart(func(b namedAndGroupHookParams) error { return nil }), ), }, } for _, tt := range table { t.Run(tt.name, func(t *testing.T) { t.Parallel() opts := fx.Options( fx.Provide(tt.annotation), fx.Invoke(func(A) {}), ) if tt.extraOpts != nil { opts = fx.Options(opts, tt.extraOpts) } if !tt.useNew { err := validateApp(t, opts) require.Error(t, err) require.Contains(t, err.Error(), tt.errContains) return } app := NewForTest(t, opts) err := app.Start(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), tt.errContains) }) } } func TestHookAnnotationFunctionFlexibility(t *testing.T) { type A any table := []struct { name string annotation any }{ { name: "without error return", annotation: fx.Annotate( func(called *atomic.Bool) A { return nil }, fx.OnStart(func(_ context.Context, called *atomic.Bool) { called.Store(true) }), ), }, { name: "without context param", annotation: fx.Annotate( func(called *atomic.Bool) A { return nil }, fx.OnStart(func(called *atomic.Bool) error { called.Store(true) return nil }), ), }, { name: "without context param or error return", annotation: fx.Annotate( func(called *atomic.Bool) A { return nil }, fx.OnStart(func(called *atomic.Bool) { called.Store(true) }), ), }, { name: "with context param and error return", annotation: fx.Annotate( func(called *atomic.Bool) A { return nil }, fx.OnStart(func(_ context.Context, called *atomic.Bool) error { called.Store(true) return nil }), ), }, } for _, tt := range table { t.Run(tt.name, func(t *testing.T) { var ( called atomic.Bool opts = fx.Options( fx.Provide(tt.annotation), fx.Supply(&called), fx.Invoke(func(A) {}), ) ) fxtest.New(t, opts).RequireStart().RequireStop() require.True(t, called.Load()) }) } } ================================================ FILE: app.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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. package fx import ( "bytes" "context" "errors" "fmt" "os" "reflect" "strings" "time" "go.uber.org/dig" "go.uber.org/fx/fxevent" "go.uber.org/fx/internal/fxclock" "go.uber.org/fx/internal/fxlog" "go.uber.org/fx/internal/fxreflect" "go.uber.org/fx/internal/lifecycle" "go.uber.org/multierr" ) // DefaultTimeout is the default timeout for starting or stopping an // application. It can be configured with the [StartTimeout] and [StopTimeout] // options. const DefaultTimeout = 15 * time.Second // An Option specifies the behavior of the application. // This is the primary means by which you interface with Fx. // // Zero or more options are specified at startup with [New]. // Options cannot be changed once an application has been initialized. // Options may be grouped into a single option using the [Options] function. // A group of options providing a logical unit of functionality // may use [Module] to name that functionality // and scope certain operations to within that module. type Option interface { fmt.Stringer apply(*module) } // Error registers any number of errors with the application to short-circuit // startup. If more than one error is given, the errors are combined into a // single error. // // Similar to invocations, errors are applied in order. All Provide and Invoke // options registered before or after an Error option will not be applied. func Error(errs ...error) Option { return errorOption(errs) } type errorOption []error func (errs errorOption) apply(mod *module) { mod.app.err = multierr.Append(mod.app.err, multierr.Combine(errs...)) } func (errs errorOption) String() string { return fmt.Sprintf("fx.Error(%v)", multierr.Combine(errs...)) } // Options bundles a group of options together into a single option. // // Use Options to group together options that don't belong in a [Module]. // // var loggingAndMetrics = fx.Options( // logging.Module, // metrics.Module, // fx.Invoke(func(logger *log.Logger) { // app.globalLogger = logger // }), // ) func Options(opts ...Option) Option { return optionGroup(opts) } type optionGroup []Option func (og optionGroup) apply(mod *module) { for _, opt := range og { opt.apply(mod) } } func (og optionGroup) String() string { items := make([]string, len(og)) for i, opt := range og { items[i] = fmt.Sprint(opt) } return fmt.Sprintf("fx.Options(%s)", strings.Join(items, ", ")) } // StartTimeout changes the application's start timeout. // This controls the total time that all [OnStart] hooks have to complete. // If the timeout is exceeded, the application will fail to start. // // Defaults to [DefaultTimeout]. func StartTimeout(v time.Duration) Option { return startTimeoutOption(v) } type startTimeoutOption time.Duration func (t startTimeoutOption) apply(m *module) { if m.parent != nil { m.app.err = fmt.Errorf("fx.StartTimeout Option should be passed to top-level App, " + "not to fx.Module") } else { m.app.startTimeout = time.Duration(t) } } func (t startTimeoutOption) String() string { return fmt.Sprintf("fx.StartTimeout(%v)", time.Duration(t)) } // StopTimeout changes the application's stop timeout. // This controls the total time that all [OnStop] hooks have to complete. // If the timeout is exceeded, the application will exit early. // // Defaults to [DefaultTimeout]. func StopTimeout(v time.Duration) Option { return stopTimeoutOption(v) } type stopTimeoutOption time.Duration func (t stopTimeoutOption) apply(m *module) { if m.parent != nil { m.app.err = fmt.Errorf("fx.StopTimeout Option should be passed to top-level App, " + "not to fx.Module") } else { m.app.stopTimeout = time.Duration(t) } } func (t stopTimeoutOption) String() string { return fmt.Sprintf("fx.StopTimeout(%v)", time.Duration(t)) } // RecoverFromPanics causes panics that occur in functions given to [Provide], // [Decorate], and [Invoke] to be recovered from. // This error can be retrieved as any other error, by using (*App).Err(). func RecoverFromPanics() Option { return recoverFromPanicsOption{} } type recoverFromPanicsOption struct{} func (o recoverFromPanicsOption) apply(m *module) { if m.parent != nil { m.app.err = fmt.Errorf("fx.RecoverFromPanics Option should be passed to top-level " + "App, not to fx.Module") } else { m.app.recoverFromPanics = true } } func (o recoverFromPanicsOption) String() string { return "fx.RecoverFromPanics()" } // WithLogger specifies the [fxevent.Logger] used by Fx to log its own events // (e.g. a constructor was provided, a function was invoked, etc.). // // The argument to this is a constructor with one of the following return // types: // // fxevent.Logger // (fxevent.Logger, error) // // The constructor may depend on any other types provided to the application. // For example, // // WithLogger(func(logger *zap.Logger) fxevent.Logger { // return &fxevent.ZapLogger{Logger: logger} // }) // // If specified, Fx will construct the logger and log all its events to the // specified logger. // // If Fx fails to build the logger, or no logger is specified, it will fall back to // [fxevent.ConsoleLogger] configured to write to stderr. func WithLogger(constructor any) Option { return withLoggerOption{ constructor: constructor, Stack: fxreflect.CallerStack(1, 0), } } type withLoggerOption struct { constructor any Stack fxreflect.Stack } func (l withLoggerOption) apply(m *module) { m.logConstructor = &provide{ Target: l.constructor, Stack: l.Stack, } } func (l withLoggerOption) String() string { return fmt.Sprintf("fx.WithLogger(%s)", fxreflect.FuncName(l.constructor)) } // Printer is the interface required by Fx's logging backend. It's implemented // by most loggers, including the one bundled with the standard library. // // Note, this will be deprecated in a future release. // Prefer to use [fxevent.Logger] instead. type Printer interface { Printf(string, ...any) } // Logger redirects the application's log output to the provided printer. // // Prefer to use [WithLogger] instead. func Logger(p Printer) Option { return loggerOption{p} } type loggerOption struct{ p Printer } func (l loggerOption) apply(m *module) { if m.parent != nil { m.app.err = fmt.Errorf("fx.Logger Option should be passed to top-level App, " + "not to fx.Module") } else { np := writerFromPrinter(l.p) m.log = fxlog.DefaultLogger(np) // assuming np is thread-safe. } } func (l loggerOption) String() string { return fmt.Sprintf("fx.Logger(%v)", l.p) } // NopLogger disables the application's log output. // // Note that this makes some failures difficult to debug, // since no errors are printed to console. // Prefer to log to an in-memory buffer instead. var NopLogger = WithLogger(func() fxevent.Logger { return fxevent.NopLogger }) // An App is a modular application built around dependency injection. Most // users will only need to use the New constructor and the all-in-one Run // convenience method. In more unusual cases, users may need to use the Err, // Start, Done, and Stop methods by hand instead of relying on Run. // // [New] creates and initializes an App. All applications begin with a // constructor for the Lifecycle type already registered. // // In addition to that built-in functionality, users typically pass a handful // of [Provide] options and one or more [Invoke] options. The Provide options // teach the application how to instantiate a variety of types, and the Invoke // options describe how to initialize the application. // // When created, the application immediately executes all the functions passed // via Invoke options. To supply these functions with the parameters they // need, the application looks for constructors that return the appropriate // types; if constructors for any required types are missing or any // invocations return an error, the application will fail to start (and Err // will return a descriptive error message). // // Once all the invocations (and any required constructors) have been called, // New returns and the application is ready to be started using Run or Start. // On startup, it executes any OnStart hooks registered with its Lifecycle. // OnStart hooks are executed one at a time, in order, and must all complete // within a configurable deadline (by default, 15 seconds). For details on the // order in which OnStart hooks are executed, see the documentation for the // Start method. // // At this point, the application has successfully started up. If started via // Run, it will continue operating until it receives a shutdown signal from // Done (see the [App.Done] documentation for details); if started explicitly via // Start, it will operate until the user calls Stop. On shutdown, OnStop hooks // execute one at a time, in reverse order, and must all complete within a // configurable deadline (again, 15 seconds by default). type App struct { err error clock fxclock.Clock lifecycle *lifecycleWrapper container *dig.Container root *module // Timeouts used startTimeout time.Duration stopTimeout time.Duration // Decides how we react to errors when building the graph. errorHooks []ErrorHandler validate bool // Whether to recover from panics in Dig container recoverFromPanics bool // Used to signal shutdowns. receivers signalReceivers osExit func(code int) // os.Exit override; used for testing only } // provide is a single constructor provided to Fx. type provide struct { // Constructor provided to Fx. This may be an fx.Annotated. Target any // Stack trace of where this provide was made. Stack fxreflect.Stack // IsSupply is true when the Target constructor was emitted by fx.Supply. IsSupply bool SupplyType reflect.Type // set only if IsSupply // Set if the type should be provided at private scope. Private bool } // invoke is a single invocation request to Fx. type invoke struct { // Function to invoke. Target any // Stack trace of where this invoke was made. Stack fxreflect.Stack } // ErrorHandler handles Fx application startup errors. // Register these with [ErrorHook]. // If specified, and the application fails to start up, // the failure will still cause a crash, // but you'll have a chance to log the error or take some other action. type ErrorHandler interface { HandleError(error) } // ErrorHook registers error handlers that implement error handling functions. // They are executed on invoke failures. Passing multiple ErrorHandlers appends // the new handlers to the application's existing list. func ErrorHook(funcs ...ErrorHandler) Option { return errorHookOption(funcs) } type errorHookOption []ErrorHandler func (eho errorHookOption) apply(m *module) { m.app.errorHooks = append(m.app.errorHooks, eho...) } func (eho errorHookOption) String() string { items := make([]string, len(eho)) for i, eh := range eho { items[i] = fmt.Sprint(eh) } return fmt.Sprintf("fx.ErrorHook(%v)", strings.Join(items, ", ")) } type errorHandlerList []ErrorHandler func (ehl errorHandlerList) HandleError(err error) { for _, eh := range ehl { eh.HandleError(err) } } // validate sets *App into validation mode without running invoked functions. func validate(validate bool) Option { return &validateOption{ validate: validate, } } type validateOption struct { validate bool } func (o validateOption) apply(m *module) { if m.parent != nil { m.app.err = fmt.Errorf("fx.validate Option should be passed to top-level App, " + "not to fx.Module") } else { m.app.validate = o.validate } } func (o validateOption) String() string { return fmt.Sprintf("fx.validate(%v)", o.validate) } // ValidateApp validates that supplied graph would run and is not missing any dependencies. This // method does not invoke actual input functions. func ValidateApp(opts ...Option) error { opts = append(opts, validate(true)) app := New(opts...) return app.Err() } // New creates and initializes an App, immediately executing any functions // registered via [Invoke] options. See the documentation of the App struct for // details on the application's initialization, startup, and shutdown logic. func New(opts ...Option) *App { logger := fxlog.DefaultLogger(os.Stderr) app := &App{ clock: fxclock.System, startTimeout: DefaultTimeout, stopTimeout: DefaultTimeout, receivers: newSignalReceivers(), } app.root = &module{ app: app, // We start with a logger that writes to stderr. One of the // following three things can change this: // // - fx.Logger was provided to change the output stream // - fx.WithLogger was provided to change the logger // implementation // - Both, fx.Logger and fx.WithLogger were provided // // The first two cases are straightforward: we use what the // user gave us. For the last case, however, we need to fall // back to what was provided to fx.Logger if fx.WithLogger // fails. log: logger, trace: []string{fxreflect.CallerStack(1, 2)[0].String()}, } for _, opt := range opts { opt.apply(app.root) } // There are a few levels of wrapping on the lifecycle here. To quickly // cover them: // // - lifecycleWrapper ensures that we don't unintentionally expose the // Start and Stop methods of the internal lifecycle.Lifecycle type // - lifecycleWrapper also adapts the internal lifecycle.Hook type into // the public fx.Hook type. // - appLogger ensures that the lifecycle always logs events to the // "current" logger associated with the fx.App. app.lifecycle = &lifecycleWrapper{ lifecycle.New(appLogger{app}, app.clock), } containerOptions := []dig.Option{ dig.DeferAcyclicVerification(), dig.DryRun(app.validate), } if app.recoverFromPanics { containerOptions = append(containerOptions, dig.RecoverFromPanics()) } app.container = dig.New(containerOptions...) app.root.build(app, app.container) // Provide Fx types first to increase the chance a custom logger // can be successfully built in the face of unrelated DI failure. // E.g., for a custom logger that relies on the Lifecycle type. frames := fxreflect.CallerStack(0, 0) // include New in the stack for default Provides app.root.provide(provide{ Target: func() Lifecycle { return app.lifecycle }, Stack: frames, }) app.root.provide(provide{Target: app.shutdowner, Stack: frames}) app.root.provide(provide{Target: app.dotGraph, Stack: frames}) app.root.provideAll() // Run decorators before executing any Invokes // (including the ones inside installAllEventLoggers). app.err = multierr.Append(app.err, app.root.decorateAll()) // If you are thinking about returning here after provides: do not (just yet)! // If a custom logger was being used, we're still buffering messages. // We'll want to flush them to the logger. // custom app logger will be initialized by the root module. app.root.installAllEventLoggers() // This error might have come from the provide loop above. We've // already flushed to the custom logger, so we can return. if app.err != nil { return app } if err := app.root.invokeAll(); err != nil { app.err = err if dig.CanVisualizeError(err) { var b bytes.Buffer dig.Visualize(app.container, &b, dig.VisualizeError(err)) err = errorWithGraph{ graph: b.String(), err: err, } } errorHandlerList(app.errorHooks).HandleError(err) } return app } func (app *App) log() fxevent.Logger { return app.root.log } // DotGraph contains a DOT language visualization of the dependency graph in // an Fx application. It is provided in the container by default at // initialization. On failure to build the dependency graph, it is attached // to the error and if possible, colorized to highlight the root cause of the // failure. // // Note that DotGraph does not yet recognize [Decorate] and [Replace]. type DotGraph string type errWithGraph interface { Graph() DotGraph } type errorWithGraph struct { graph string err error } func (err errorWithGraph) Graph() DotGraph { return DotGraph(err.graph) } func (err errorWithGraph) Error() string { return err.err.Error() } // VisualizeError returns the visualization of the error if available. // // Note that VisualizeError does not yet recognize [Decorate] and [Replace]. func VisualizeError(err error) (string, error) { var erg errWithGraph if errors.As(err, &erg) { if g := erg.Graph(); g != "" { return string(g), nil } } return "", errors.New("unable to visualize error") } // Exits the application with the given exit code. func (app *App) exit(code int) { osExit := os.Exit if app.osExit != nil { osExit = app.osExit } osExit(code) } // Run starts the application, blocks on the signals channel, and then // gracefully shuts the application down. It uses [DefaultTimeout] to set a // deadline for application startup and shutdown, unless the user has // configured different timeouts with the [StartTimeout] or [StopTimeout] options. // It's designed to make typical applications simple to run. // The minimal Fx application looks like this: // // fx.New().Run() // // All of Run's functionality is implemented in terms of the exported // Start, Done, and Stop methods. Applications with more specialized needs // can use those methods directly instead of relying on Run. // // After the application has started, // it can be shut down by sending a signal or calling [Shutdowner.Shutdown]. // On successful shutdown, whether initiated by a signal or by the user, // Run will return to the caller, allowing it to exit cleanly. // Run will exit with a non-zero status code // if startup or shutdown operations fail, // or if the [Shutdowner] supplied a non-zero exit code. func (app *App) Run() { // Historically, we do not os.Exit(0) even though most applications // cede control to Fx with they call app.Run. To avoid a breaking // change, never os.Exit for success. if code := app.run(app.Wait); code != 0 { app.exit(code) } } func (app *App) run(done func() <-chan ShutdownSignal) (exitCode int) { startCtx, cancel := app.clock.WithTimeout(context.Background(), app.StartTimeout()) defer cancel() if err := app.Start(startCtx); err != nil { return 1 } sig := <-done() app.log().LogEvent(&fxevent.Stopping{Signal: sig.Signal}) exitCode = sig.ExitCode stopCtx, cancel := app.clock.WithTimeout(context.Background(), app.StopTimeout()) defer cancel() if err := app.Stop(stopCtx); err != nil { return 1 } return exitCode } // Err returns any error encountered during New's initialization. See the // documentation of the New method for details, but typical errors include // missing constructors, circular dependencies, constructor errors, and // invocation errors. // // Most users won't need to use this method, since both Run and Start // short-circuit if initialization failed. func (app *App) Err() error { return app.err } var ( _onStartHook = "OnStart" _onStopHook = "OnStop" ) // Start kicks off all long-running goroutines, like network servers or // message queue consumers. It does this by interacting with the application's // Lifecycle. // // By taking a dependency on the Lifecycle type, some of the user-supplied // functions called during initialization may have registered start and stop // hooks. Because initialization calls constructors serially and in dependency // order, hooks are naturally registered in serial and dependency order too. // // Start executes all OnStart hooks registered with the application's // Lifecycle, one at a time and in order. This ensures that each constructor's // start hooks aren't executed until all its dependencies' start hooks // complete. If any of the start hooks return an error, Start short-circuits, // calls Stop, and returns the inciting error. // // Note that Start short-circuits immediately if the New constructor // encountered any errors in application initialization. func (app *App) Start(ctx context.Context) (err error) { defer func() { app.log().LogEvent(&fxevent.Started{Err: err}) }() if app.err != nil { // Some provides failed, short-circuit immediately. return app.err } return withTimeout(ctx, &withTimeoutParams{ hook: _onStartHook, callback: app.start, lifecycle: app.lifecycle, log: app.log(), }) } // withRollback will execute an anonymous function with a given context. // if the anon func returns an error, rollback methods will be called and related events emitted func (app *App) withRollback( ctx context.Context, f func(context.Context) error, ) error { if err := f(ctx); err != nil { app.log().LogEvent(&fxevent.RollingBack{StartErr: err}) stopErr := app.lifecycle.Stop(ctx) app.log().LogEvent(&fxevent.RolledBack{Err: stopErr}) if stopErr != nil { return multierr.Append(err, stopErr) } return err } return nil } func (app *App) start(ctx context.Context) error { return app.withRollback(ctx, func(ctx context.Context) error { if err := app.lifecycle.Start(ctx); err != nil { return err } return nil }) } // Stop gracefully stops the application. It executes any registered OnStop // hooks in reverse order, so that each constructor's stop hooks are called // before its dependencies' stop hooks. // // If the application didn't start cleanly, only hooks whose OnStart phase was // called are executed. However, all those hooks are executed, even if some // fail. func (app *App) Stop(ctx context.Context) (err error) { defer func() { app.log().LogEvent(&fxevent.Stopped{Err: err}) }() cb := func(ctx context.Context) error { defer app.receivers.Stop(ctx) return app.lifecycle.Stop(ctx) } return withTimeout(ctx, &withTimeoutParams{ hook: _onStopHook, callback: cb, lifecycle: app.lifecycle, log: app.log(), }) } // Done returns a channel of signals to block on after starting the // application. Applications listen for the SIGINT and SIGTERM signals; during // development, users can send the application SIGTERM by pressing Ctrl-C in // the same terminal as the running process. // // Alternatively, a signal can be broadcast to all done channels manually by // using the Shutdown functionality (see the [Shutdowner] documentation for details). func (app *App) Done() <-chan os.Signal { app.receivers.Start() // No-op if running return app.receivers.Done() } // Wait returns a channel of [ShutdownSignal] to block on after starting the // application and function, similar to [App.Done], but with a minor difference: // if the app was shut down via [Shutdowner.Shutdown], // the exit code (if provied via [ExitCode]) will be available // in the [ShutdownSignal] struct. // Otherwise, the signal that was received will be set. func (app *App) Wait() <-chan ShutdownSignal { app.receivers.Start() // No-op if running return app.receivers.Wait() } // StartTimeout returns the configured startup timeout. // This defaults to [DefaultTimeout], and can be changed with the // [StartTimeout] option. func (app *App) StartTimeout() time.Duration { return app.startTimeout } // StopTimeout returns the configured shutdown timeout. // This defaults to [DefaultTimeout], and can be changed with the // [StopTimeout] option. func (app *App) StopTimeout() time.Duration { return app.stopTimeout } func (app *App) dotGraph() (DotGraph, error) { var b bytes.Buffer err := dig.Visualize(app.container, &b) return DotGraph(b.String()), err } type withTimeoutParams struct { log fxevent.Logger hook string callback func(context.Context) error lifecycle *lifecycleWrapper } // errHookCallbackExited is returned when a hook callback does not finish executing var errHookCallbackExited = errors.New("goroutine exited without returning") func withTimeout(ctx context.Context, param *withTimeoutParams) error { c := make(chan error, 1) go func() { // If runtime.Goexit() is called from within the callback // then nothing is written to the chan. // However the defer will still be called, so we can write to the chan, // to avoid hanging until the timeout is reached. callbackExited := false defer func() { if !callbackExited { c <- errHookCallbackExited } }() c <- param.callback(ctx) callbackExited = true }() var err error select { case <-ctx.Done(): err = ctx.Err() case err = <-c: // If the context finished at the same time as the callback // prefer the context error. // This eliminates non-determinism in select-case selection. if ctx.Err() != nil { err = ctx.Err() } } return err } // appLogger logs events to the given Fx app's "current" logger. // // Use this with lifecycle, for example, to ensure that events always go to the // correct logger. type appLogger struct{ app *App } func (l appLogger) LogEvent(ev fxevent.Event) { l.app.log().LogEvent(ev) } ================================================ FILE: app_internal_test.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fx import ( "context" "errors" "fmt" "os" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/fxevent" "go.uber.org/fx/internal/fxclock" "go.uber.org/fx/internal/fxlog" "go.uber.org/fx/internal/fxreflect" ) func TestAppRun(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) app := New( WithLogger(func() fxevent.Logger { return spy }), ) done := make(chan ShutdownSignal) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() app.run(func() <-chan ShutdownSignal { return done }) }() done <- ShutdownSignal{Signal: _sigINT} wg.Wait() assert.Equal(t, []string{ "Provided", "Provided", "Provided", "LoggerInitialized", "Started", "Stopping", "Stopped", }, spy.EventTypes()) } // TestValidateString verifies private option. Public options are tested in app_test.go. func TestValidateString(t *testing.T) { t.Parallel() stringer, ok := validate(true).(fmt.Stringer) require.True(t, ok, "option must implement stringer") assert.Equal(t, "fx.validate(true)", stringer.String()) } // WithExit is an internal option available only to tests defined in this // package. It changes how os.Exit behaves for the application. func WithExit(f func(int)) Option { return withExitOption(f) } type withExitOption func(int) func (o withExitOption) String() string { return fmt.Sprintf("WithExit(%v)", fxreflect.FuncName(o)) } func (o withExitOption) apply(m *module) { m.app.osExit = o } // WithClock specifies how Fx accesses time operations. // // This is an internal option available only to tests defined in this package. func WithClock(clock fxclock.Clock) Option { return withClockOption{clock} } type withClockOption struct{ clock fxclock.Clock } func (o withClockOption) apply(m *module) { m.app.clock = o.clock } func (o withClockOption) String() string { return fmt.Sprintf("WithClock(%v)", o.clock) } func TestAnnotationError(t *testing.T) { wantErr := errors.New("want error") err := &annotationError{ err: wantErr, } require.Error(t, err) assert.ErrorIs(t, err, wantErr) assert.Contains(t, err.Error(), wantErr.Error()) } // TestStartDoesNotRegisterSignals verifies that signal.Notify is not called // when a user starts an app. signal.Notify should only be called when the // .Wait/.Done are called. Note that app.Run calls .Wait() implicitly. func TestStartDoesNotRegisterSignals(t *testing.T) { app := New() calledNotify := false // Mock notify function to spy when this is called. app.receivers.notify = func(c chan<- os.Signal, sig ...os.Signal) { calledNotify = true } app.receivers.stopNotify = func(c chan<- os.Signal) {} app.Start(context.Background()) defer app.Stop(context.Background()) assert.False(t, calledNotify, "notify should not be called when app starts") _ = app.Wait() // User signals intent have fx listen for signals. This should call notify assert.True(t, calledNotify, "notify should be called after Wait") } ================================================ FILE: app_test.go ================================================ // Copyright (c) 2023 Uber Technologies, Inc. // // 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. package fx_test import ( "bytes" "context" "errors" "fmt" "io" "log" "os" "reflect" "regexp" "runtime" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" . "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/fx/fxtest" "go.uber.org/fx/internal/fxclock" "go.uber.org/fx/internal/fxlog" "go.uber.org/goleak" "go.uber.org/multierr" "go.uber.org/zap" ) func NewForTest(tb testing.TB, opts ...Option) *App { testOpts := []Option{ // Provide both: Logger and WithLogger so that if the test // WithLogger fails, we don't pollute stderr. Logger(fxtest.NewTestPrinter(tb)), fxtest.WithTestLogger(tb), } opts = append(testOpts, opts...) return New(opts...) } func NewSpied(opts ...Option) (*App, *fxlog.Spy) { spy := new(fxlog.Spy) opts = append([]Option{ WithLogger(func() fxevent.Logger { return spy }), }, opts...) return New(opts...), spy } func validateTestApp(tb testing.TB, opts ...Option) error { testOpts := []Option{ // Provide both: Logger and WithLogger so that if the test // WithLogger fails, we don't pollute stderr. Logger(fxtest.NewTestPrinter(tb)), fxtest.WithTestLogger(tb), } opts = append(testOpts, opts...) return ValidateApp(opts...) } func TestNewApp(t *testing.T) { t.Parallel() t.Run("ProvidesLifecycleAndShutdowner", func(t *testing.T) { t.Parallel() var ( l Lifecycle s Shutdowner ) fxtest.New( t, Populate(&l, &s), ) assert.NotNil(t, l) assert.NotNil(t, s) }) t.Run("OptionsHappensBeforeProvides", func(t *testing.T) { t.Parallel() // Should be grouping all provides and pushing them into the container // after applying other options. This prevents the app configuration // (e.g., logging) from changing halfway through our provides. spy := new(fxlog.Spy) app := fxtest.New(t, Provide(func() struct{} { return struct{}{} }), WithLogger(func() fxevent.Logger { return spy })) defer app.RequireStart().RequireStop() require.Equal(t, []string{"Provided", "Provided", "Provided", "Provided", "LoggerInitialized", "Started"}, spy.EventTypes()) // Fx types get provided first to increase chance of // successful custom logger build. assert.Contains(t, spy.Events()[0].(*fxevent.Provided).OutputTypeNames, "fx.Lifecycle") assert.Contains(t, spy.Events()[1].(*fxevent.Provided).OutputTypeNames, "fx.Shutdowner") assert.Contains(t, spy.Events()[2].(*fxevent.Provided).OutputTypeNames, "fx.DotGraph") // Our type should be index 3. assert.Contains(t, spy.Events()[3].(*fxevent.Provided).OutputTypeNames, "struct {}") }) t.Run("CircularGraphReturnsError", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} app := NewForTest(t, Provide(func(A) B { return B{} }), Provide(func(B) A { return A{} }), Invoke(func(B) {}), ) err := app.Err() require.Error(t, err, "fx.New should return an error") errMsg := err.Error() assert.Contains(t, errMsg, "cycle detected in dependency graph") assert.Contains(t, errMsg, "depends on func(fx_test.B) fx_test.A") assert.Contains(t, errMsg, "depends on func(fx_test.A) fx_test.B") }) t.Run("ProvidesDotGraph", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} type C struct{} var g DotGraph app := fxtest.New(t, Provide(func() A { return A{} }), Provide(func(A) B { return B{} }), Provide(func(A, B) C { return C{} }), Populate(&g), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) assert.Contains(t, g, `"fx.DotGraph" [label=];`) }) t.Run("ProvidesWithAnnotate", func(t *testing.T) { t.Parallel() type A struct{} type B struct { In Foo A `name:"foo"` Bar A `name:"bar"` Foos []A `group:"foo"` } app := fxtest.New(t, Provide( Annotated{ Target: func() A { return A{} }, Name: "foo", }, Annotated{ Target: func() A { return A{} }, Name: "bar", }, Annotated{ Target: func() A { return A{} }, Group: "foo", }, ), Invoke( func(b B) { assert.NotNil(t, b.Foo) assert.NotNil(t, b.Bar) assert.Len(t, b.Foos, 1) }, ), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("ProvidesWithAnnotateFlattened", func(t *testing.T) { t.Parallel() app := fxtest.New(t, Provide(Annotated{ Target: func() []int { return []int{1} }, Group: "foo,flatten", }), Invoke( func(b struct { In Foos []int `group:"foo"` }, ) { assert.Len(t, b.Foos, 1) }, ), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("ProvidesWithEmptyAnnotate", func(t *testing.T) { t.Parallel() type A struct{} type B struct { In Foo A } app := fxtest.New(t, Provide( Annotated{ Target: func() A { return A{} }, }, ), Invoke( func(b B) { assert.NotNil(t, b.Foo) }, ), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("CannotNameAndGroup", func(t *testing.T) { t.Parallel() type A struct{} app := NewForTest(t, Provide( Annotated{ Target: func() A { return A{} }, Name: "foo", Group: "bar", }, ), ) err := app.Err() require.Error(t, err) // fx.Annotated may specify only one of Name or Group: received fx.Annotated{Name: "foo", Group: "bar", Target: go.uber.org/fx_test.TestAnnotatedWithGroupAndName.func1()} from: // go.uber.org/fx_test.TestAnnotatedWithGroupAndName // /.../fx/annotated_test.go:164 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 assert.Contains(t, err.Error(), "fx.Annotated may specify only one of Name or Group:") assert.Contains(t, err.Error(), `received fx.Annotated{Name: "foo", Group: "bar", Target: go.uber.org/fx_test.TestNewApp`) assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestNewApp") assert.Contains(t, err.Error(), "/app_test.go") }) t.Run("ErrorProvidingAnnotated", func(t *testing.T) { t.Parallel() app := NewForTest(t, Provide(Annotated{ Target: 42, // not a constructor Name: "foo", })) err := app.Err() require.Error(t, err) // Example: // fx.Provide(fx.Annotated{...}) from: // go.uber.org/fx_test.TestNewApp.func8 // /.../fx/app_test.go:206 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 // Failed: must provide constructor function, got 42 (type int) assert.Contains(t, err.Error(), `fx.Provide(fx.Annotated{Name: "foo", Target: 42}) from:`) assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestNewApp") assert.Contains(t, err.Error(), "/app_test.go") assert.Contains(t, err.Error(), "Failed: must provide constructor function") }) t.Run("ErrorProvidingAnnotate", func(t *testing.T) { t.Parallel() type t1 struct{} newT1 := func() t1 { return t1{} } // Provide twice. app := NewForTest(t, Provide( Annotate(newT1, ResultTags(`name:"foo"`)), Annotate(newT1, ResultTags(`name:"foo"`)), )) err := app.Err() require.Error(t, err) // Example: // fx.Provide(fx.Annotate(go.uber.org/fx_test.TestNewApp.func10.1(), fx.ResultTags(["name:\"foo\""])) from: // go.uber.org/fx_test.TestNewApp.func10 // /.../fx/app_test.go:305 // testing.tRunner // /.../src/testing/testing.go:1259 // Failed: cannot provide function "reflect".makeFuncStub (/.../reflect/asm_amd64.s:30): // cannot provide fx_test.t1[name="foo"] from [0].Field0: // already provided by "reflect".makeFuncStub (/.../reflect/asm_amd64.s:30) assert.Contains(t, err.Error(), `fx.Provide(fx.Annotate(`) assert.Contains(t, err.Error(), `fx.ResultTags(["name:\"foo\""])`) assert.Contains(t, err.Error(), "already provided") }) t.Run("ErrorProviding", func(t *testing.T) { t.Parallel() err := NewForTest(t, Provide(42)).Err() require.Error(t, err) // Example: // fx.Provide(..) from: // go.uber.org/fx_test.TestNewApp.func8 // /.../fx/app_test.go:206 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 // Failed: must provide constructor function, got 42 (type int) assert.Contains(t, err.Error(), "fx.Provide(42) from:") assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestNewApp") assert.Contains(t, err.Error(), "/app_test.go") assert.Contains(t, err.Error(), "Failed: must provide constructor function") }) t.Run("Decorates", func(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) type A struct{ value int } app := fxtest.New(t, Provide(func() A { return A{value: 0} }), Decorate(func(a A) A { return A{value: a.value + 1} }), Invoke(func(a A) { assert.Equal(t, a.value, 1) }), WithLogger(func() fxevent.Logger { return spy })) defer app.RequireStart().RequireStop() require.Equal(t, []string{"Provided", "Provided", "Provided", "Provided", "Decorated", "LoggerInitialized", "Invoking", "BeforeRun", "Run", "BeforeRun", "Run", "Invoked", "Started"}, spy.EventTypes()) }) t.Run("DecoratesFromManyModules", func(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) type A struct{ value int } m := Module("decorator", Decorate(func(a A) A { return A{value: a.value + 1} }), ) app := fxtest.New(t, m, Provide(func() A { return A{value: 0} }), Decorate(func(a A) A { return A{value: a.value + 1} }), WithLogger(func() fxevent.Logger { return spy }), ) defer app.RequireStart().RequireStop() require.Equal(t, []string{"Provided", "Provided", "Provided", "Provided", "Decorated", "Decorated", "LoggerInitialized", "Started"}, spy.EventTypes()) }) } // TestPrivate tests Private when used with both fx.Provide and fx.Supply. func TestPrivate(t *testing.T) { t.Parallel() testCases := []struct { desc string // provide is either a Supply or Provide wrapper around the given int // that allows us to generalize these test cases for both APIs. provide func(int, bool) Option }{ { desc: "Provide", provide: func(i int, private bool) Option { opts := []any{func() int { return i }} if private { opts = append(opts, Private) } return Provide(opts...) }, }, { desc: "Supply", provide: func(i int, private bool) Option { opts := []any{i} if private { opts = append(opts, Private) } return Supply(opts...) }, }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { t.Parallel() t.Run("CanUsePrivateFromParentModule", func(t *testing.T) { t.Parallel() var invoked bool app := fxtest.New(t, Module("SubModule", Invoke(func(a int, b string) { assert.Equal(t, 0, a) invoked = true })), Provide(func() string { return "" }), tt.provide(0, true /* private */), ) app.RequireStart().RequireStop() assert.True(t, invoked) }) t.Run("CannotUsePrivateFromSubModule", func(t *testing.T) { t.Parallel() app := NewForTest(t, Module("SubModule", tt.provide(0, true /* private */)), Invoke(func(a int) {}), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "missing dependencies for function") assert.Contains(t, err.Error(), "missing type: int") }) t.Run("MultipleModulesSameType", func(t *testing.T) { t.Parallel() var invoked int app := fxtest.New(t, Module("SubModuleA", tt.provide(1, true /* private */), Invoke(func(s int) { assert.Equal(t, 1, s) invoked++ }), ), Module("SubModuleB", tt.provide(2, true /* private */), Invoke(func(s int) { assert.Equal(t, 2, s) invoked++ }), ), tt.provide(3, false /* private */), Invoke(func(s int) { assert.Equal(t, 3, s) invoked++ }), ) app.RequireStart().RequireStop() assert.Equal(t, 3, invoked) }) }) } } func TestPrivateProvideWithDecorators(t *testing.T) { t.Parallel() testCases := []struct { desc string // provide is either a Supply or Provide wrapper around the given int // that allows us to generalize these test cases for both APIs. provide func(int) Option private bool }{ { desc: "Private/Provide", provide: func(i int) Option { return Provide( func() int { return i }, Private, ) }, private: true, }, { desc: "Private/Supply", provide: func(i int) Option { return Supply(i, Private) }, private: true, }, { desc: "Public/Provide", provide: func(i int) Option { return Provide(func() int { return i }) }, private: false, }, { desc: "Public/Supply", provide: func(i int) Option { return Supply(i) }, private: false, }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { t.Parallel() t.Run("DecoratedTypeInSubModule", func(t *testing.T) { t.Parallel() var invoked bool app := NewForTest(t, Module("SubModule", tt.provide(0), Decorate(func(a int) int { return a + 2 }), Invoke(func(a int) { assert.Equal(t, 2, a) }), ), Invoke(func(a int) { // Decoration is always "private", // so raw provided value is expected here // when the submodule provides it as public. assert.Equal(t, 0, a) invoked = true }), ) err := app.Err() if tt.private { require.Error(t, err) assert.Contains(t, err.Error(), "missing dependencies for function") assert.Contains(t, err.Error(), "missing type: int") } else { require.NoError(t, err) assert.True(t, invoked) } }) t.Run("DecoratedTypeInParentModule", func(t *testing.T) { t.Parallel() var invoked int app := fxtest.New(t, tt.provide(0), Decorate(func(a int) int { return a - 5 }), Invoke(func(a int) { assert.Equal(t, -5, a) invoked++ }), Module("Child", Decorate(func(a int) int { return a + 10 }), Invoke(func(a int) { assert.Equal(t, 5, a) invoked++ }), ), ) app.RequireStart().RequireStop() assert.Equal(t, 2, invoked) }) t.Run("ParentDecorateChildType", func(t *testing.T) { t.Parallel() var invoked bool app := NewForTest(t, Module("Child", tt.provide(0)), Decorate(func(a int) int { return a + 5 }), Invoke(func(a int) { invoked = true }), ) err := app.Err() if tt.private { require.Error(t, err) assert.Contains(t, err.Error(), "missing dependencies for function") assert.Contains(t, err.Error(), "missing type: int") } else { require.NoError(t, err) assert.True(t, invoked) } }) }) } } func TestWithLoggerErrorUseDefault(t *testing.T) { // This test cannot be run in paralllel with the others because // it hijacks stderr. // Temporarily hijack stderr and restore it after this test so // that we can assert its contents. f, err := os.CreateTemp(t.TempDir(), "stderr") if err != nil { t.Fatalf("could not open a file for writing") } defer func(oldStderr *os.File) { assert.NoError(t, f.Close()) os.Stderr = oldStderr }(os.Stderr) os.Stderr = f app := New( Supply(zap.NewNop()), WithLogger(&bytes.Buffer{}), ) err = app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "must provide constructor function, got (type *bytes.Buffer)", ) stderr, err := os.ReadFile(f.Name()) require.NoError(t, err) // Example output: // [Fx] SUPPLY *zap.Logger // [Fx] ERROR Failed to initialize custom logger: fx.WithLogger() from: // go.uber.org/fx_test.TestSetupLogger.func3 // /Users/abg/dev/fx/app_test.go:334 // testing.tRunner // /usr/local/Cellar/go/1.16.4/libexec/src/testing/testing.go:1193 // Failed: must provide constructor function, got (type *bytes.Buffer) out := string(stderr) assert.Contains(t, out, "[Fx] SUPPLY\t*zap.Logger\n") assert.Contains(t, out, "[Fx] ERROR\t\tFailed to initialize custom logger: fx.WithLogger") assert.Contains(t, out, "must provide constructor function, got (type *bytes.Buffer)\n") } func TestWithLogger(t *testing.T) { t.Parallel() t.Run("initializing custom logger", func(t *testing.T) { t.Parallel() var spy fxlog.Spy app := fxtest.New(t, Supply(&spy), WithLogger(func(spy *fxlog.Spy) fxevent.Logger { return spy }), ) assert.Equal(t, []string{ "Provided", "Provided", "Provided", "Supplied", "BeforeRun", "Run", "LoggerInitialized", }, spy.EventTypes()) spy.Reset() app.RequireStart().RequireStop() require.NoError(t, app.Err()) assert.Equal(t, []string{"Started", "Stopped"}, spy.EventTypes()) }) t.Run("error in Provide shows logs", func(t *testing.T) { t.Parallel() var spy fxlog.Spy app := New( Supply(&spy), WithLogger(func(spy *fxlog.Spy) fxevent.Logger { return spy }), Provide(&bytes.Buffer{}), // not passing in a constructor. ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "must provide constructor function, got (type *bytes.Buffer)", ) assert.Equal(t, []string{"Provided", "Provided", "Provided", "Supplied", "Provided", "BeforeRun", "Run", "LoggerInitialized"}, spy.EventTypes()) }) t.Run("logger failed to build", func(t *testing.T) { t.Parallel() var buff bytes.Buffer app := New( Logger(log.New(&buff, "", 0)), WithLogger(func() (fxevent.Logger, error) { return nil, errors.New("great sadness") }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "great sadness") out := buff.String() assert.Contains(t, out, "[Fx] ERROR\t\tFailed to initialize custom logger") }) t.Run("logger dependency failed to build", func(t *testing.T) { t.Parallel() var buff bytes.Buffer app := New( Logger(log.New(&buff, "", 0)), Provide(func() (*zap.Logger, error) { return nil, errors.New("great sadness") }), WithLogger(func(log *zap.Logger) fxevent.Logger { t.Errorf("WithLogger must not be called") panic("must not be called") }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "great sadness") out := buff.String() assert.Contains(t, out, "[Fx] PROVIDE\t*zap.Logger") assert.Contains(t, out, "[Fx] ERROR\t\tFailed to initialize custom logger") }) } func getInt() int { return 0 } func decorateInt(i int) int { return i } var moduleA = Module( "ModuleA", Provide(getInt), Decorate(decorateInt), Supply(int64(14)), Replace("foo"), ) func getModuleB() Option { return Module( "ModuleB", moduleA, ) } func TestModuleTrace(t *testing.T) { t.Parallel() moduleC := Module( "ModuleC", getModuleB(), ) app, spy := NewSpied(moduleC) require.NoError(t, app.Err()) wantTrace, err := regexp.Compile( // Provide/decorate itself, initialized via init. "^go.uber.org/fx_test.init \\(.*fx/app_test.go:.*\\)\n" + // ModuleA initialized via init. "go.uber.org/fx_test.init \\(.*fx/app_test.go:.*\\) \\(ModuleA\\)\n" + // ModuleB from getModuleB. "go.uber.org/fx_test.getModuleB \\(.*fx/app_test.go:.*\\) \\(ModuleB\\)\n" + // ModuleC above. "go.uber.org/fx_test.TestModuleTrace \\(.*fx/app_test.go:.*\\) \\(ModuleC\\)\n" + // Top-level app & corresponding module created by NewSpied. "go.uber.org/fx_test.NewSpied \\(.*fx/app_test.go:.*\\)$", ) require.NoError(t, err, "test regexp compilation error") for _, tt := range []struct { desc string getTrace func(t *testing.T) []string }{ { desc: "Provide", getTrace: func(t *testing.T) []string { t.Helper() var event *fxevent.Provided for _, e := range spy.Events().SelectByTypeName("Provided") { pe, ok := e.(*fxevent.Provided) if !ok { continue } if strings.HasSuffix(pe.ConstructorName, "getInt()") { event = pe break } } require.NotNil(t, event, "could not find provide event for getInt()") return event.ModuleTrace }, }, { desc: "Decorate", getTrace: func(t *testing.T) []string { t.Helper() events := spy.Events().SelectByTypeName("Decorated") require.Len(t, events, 1) event, ok := events[0].(*fxevent.Decorated) require.True(t, ok) return event.ModuleTrace }, }, { desc: "Supply", getTrace: func(t *testing.T) []string { t.Helper() events := spy.Events().SelectByTypeName("Supplied") require.Len(t, events, 1) event, ok := events[0].(*fxevent.Supplied) require.True(t, ok) return event.ModuleTrace }, }, { desc: "Replaced", getTrace: func(t *testing.T) []string { t.Helper() events := spy.Events().SelectByTypeName("Replaced") require.Len(t, events, 1) event, ok := events[0].(*fxevent.Replaced) require.True(t, ok) return event.ModuleTrace }, }, } { t.Run(tt.desc, func(t *testing.T) { gotTrace := strings.Join(tt.getTrace(t), "\n") assert.Regexp(t, wantTrace, gotTrace) }) } } func TestRunEventEmission(t *testing.T) { t.Parallel() for _, tt := range []struct { desc string giveOpts []Option wantRunEvents []fxevent.Run wantErr string }{ { desc: "Simple Provide And Decorate", giveOpts: []Option{ Provide(func() int { return 5 }), Decorate(func(int) int { return 6 }), Invoke(func(int) {}), }, wantRunEvents: []fxevent.Run{ { Name: "go.uber.org/fx_test.TestRunEventEmission.func1()", Kind: "provide", }, { Name: "go.uber.org/fx_test.TestRunEventEmission.func2()", Kind: "decorate", }, }, }, { desc: "Supply and Decorator Error", giveOpts: []Option{ Supply(5), Decorate(func(int) (int, error) { return 0, errors.New("humongous despair") }), Invoke(func(int) {}), }, wantRunEvents: []fxevent.Run{ { Name: "stub(int)", Kind: "supply", }, { Name: "go.uber.org/fx_test.TestRunEventEmission.func4()", Kind: "decorate", }, }, wantErr: "humongous despair", }, { desc: "Replace", giveOpts: []Option{ Provide(func() int { return 5 }), Replace(6), Invoke(func(int) {}), }, wantRunEvents: []fxevent.Run{ { Name: "stub(int)", Kind: "replace", }, }, }, { desc: "Provide Error", giveOpts: []Option{ Provide(func() (int, error) { return 0, errors.New("terrible sadness") }), Invoke(func(int) {}), }, wantRunEvents: []fxevent.Run{ { Name: "go.uber.org/fx_test.TestRunEventEmission.func8()", Kind: "provide", }, }, wantErr: "terrible sadness", }, { desc: "Provide Panic", giveOpts: []Option{ Provide(func() int { panic("bad provide") }), RecoverFromPanics(), Invoke(func(int) {}), }, wantRunEvents: []fxevent.Run{ { Name: "go.uber.org/fx_test.TestRunEventEmission.func10()", Kind: "provide", }, }, wantErr: `panic: "bad provide"`, }, { desc: "Decorate Panic", giveOpts: []Option{ Supply(5), Decorate(func(int) int { panic("bad decorate") }), RecoverFromPanics(), Invoke(func(int) {}), }, wantRunEvents: []fxevent.Run{ { Name: "stub(int)", Kind: "supply", }, { Name: "go.uber.org/fx_test.TestRunEventEmission.func12()", Kind: "decorate", }, }, wantErr: `panic: "bad decorate"`, }, } { t.Run(tt.desc, func(t *testing.T) { t.Parallel() app, spy := NewSpied(tt.giveOpts...) if tt.wantErr != "" { assert.ErrorContains(t, app.Err(), tt.wantErr) } else { assert.NoError(t, app.Err()) } gotBeforeEvents := spy.Events().SelectByTypeName("BeforeRun") gotEvents := spy.Events().SelectByTypeName("Run") require.Len(t, gotBeforeEvents, len(tt.wantRunEvents), "wrong number of before-run events") require.Len(t, gotEvents, len(tt.wantRunEvents), "wrong number of run events") // BeforeRun events are just a reduced-field version of Run events for i, event := range gotBeforeEvents { rEvent, ok := event.(*fxevent.BeforeRun) require.True(t, ok) assert.Equal(t, tt.wantRunEvents[i].Name, rEvent.Name) assert.Equal(t, tt.wantRunEvents[i].Kind, rEvent.Kind) } for i, event := range gotEvents { rEvent, ok := event.(*fxevent.Run) require.True(t, ok) assert.Equal(t, tt.wantRunEvents[i].Name, rEvent.Name) assert.Equal(t, tt.wantRunEvents[i].Kind, rEvent.Kind) if tt.wantErr != "" && i == len(gotEvents)-1 { assert.ErrorContains(t, rEvent.Err, tt.wantErr) } else { assert.NoError(t, rEvent.Err) } } }) } } type customError struct { err error } func (e *customError) Error() string { return fmt.Sprintf("custom error: %v", e.err) } func (e *customError) Unwrap() error { return e.err } type errHandlerFunc func(error) func (f errHandlerFunc) HandleError(err error) { f(err) } func TestInvokes(t *testing.T) { t.Parallel() t.Run("Success event", func(t *testing.T) { t.Parallel() app, spy := NewSpied( Invoke(func() {}), ) require.NoError(t, app.Err()) invoked := spy.Events().SelectByTypeName("Invoked") require.Len(t, invoked, 1) assert.NoError(t, invoked[0].(*fxevent.Invoked).Err) }) t.Run("Failure event", func(t *testing.T) { t.Parallel() wantErr := errors.New("great sadness") app, spy := NewSpied( Invoke(func() error { return wantErr }), ) require.Error(t, app.Err()) invoked := spy.Events().SelectByTypeName("Invoked") require.Len(t, invoked, 1) require.Error(t, invoked[0].(*fxevent.Invoked).Err) require.ErrorIs(t, invoked[0].(*fxevent.Invoked).Err, wantErr) }) t.Run("ErrorsAreNotOverriden", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} app := NewForTest(t, Provide(func() B { return B{} }), // B inserted into the graph Invoke(func(A) {}), // failed A invoke Invoke(func(B) {}), // successful B invoke ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "missing type: fx_test.A") }) t.Run("ErrorHooksAreCalled", func(t *testing.T) { t.Parallel() type A struct{} count := 0 h := errHandlerFunc(func(err error) { count++ }) NewForTest(t, Invoke(func(A) {}), ErrorHook(h), ) assert.Equal(t, 1, count) }) t.Run("ErrorsAreWrapped", func(t *testing.T) { t.Parallel() wantErr := errors.New("err") app := NewForTest(t, Invoke(func() error { return &customError{err: wantErr} }), ) err := app.Err() require.Error(t, err) assert.ErrorIs(t, err, wantErr) var ce *customError assert.ErrorAs(t, err, &ce) }) } func TestError(t *testing.T) { t.Parallel() t.Run("NilErrorOption", func(t *testing.T) { t.Parallel() var invoked bool app := NewForTest(t, Error(nil), Invoke(func() { invoked = true }), ) err := app.Err() require.NoError(t, err) assert.True(t, invoked) }) t.Run("SingleErrorOption", func(t *testing.T) { t.Parallel() wantErr := errors.New("module failure") app := NewForTest(t, Error(wantErr), Invoke(func() { t.Errorf("Invoke should not be called") }), ) err := app.Err() require.Error(t, err) assert.EqualError(t, err, "module failure") assert.ErrorIs(t, err, wantErr) }) t.Run("MultipleErrorOption", func(t *testing.T) { t.Parallel() type A struct{} errA := errors.New("module A failure") errB := errors.New("module B failure") app := NewForTest(t, Provide(func() A { t.Errorf("Provide should not be called") return A{} }, ), Invoke(func(A) { t.Errorf("Invoke should not be called") }), Error( errA, errB, ), ) err := app.Err() require.Error(t, err) assert.ErrorIs(t, err, errA) assert.ErrorIs(t, err, errB) assert.NotContains(t, err.Error(), "not in the container") }) t.Run("ProvideAndInvokeErrorsAreIgnored", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} app := NewForTest(t, Provide(func(b B) A { t.Errorf("B is missing from the container; Provide should not be called") return A{} }, ), Error(errors.New("module failure")), Invoke(func(A) { t.Errorf("A was not provided; Invoke should not be called") }), ) err := app.Err() assert.EqualError(t, err, "module failure") }) } func TestOptions(t *testing.T) { t.Parallel() t.Run("OptionsComposition", func(t *testing.T) { t.Parallel() var n int construct := func() struct{} { n++ return struct{}{} } use := func(struct{}) { n++ } app := fxtest.New(t, Options(Provide(construct), Invoke(use))) defer app.RequireStart().RequireStop() assert.Equal(t, 2, n) }) t.Run("ProvidesCalledInGraphOrder", func(t *testing.T) { t.Parallel() type type1 struct{} type type2 struct{} type type3 struct{} initOrder := 0 new1 := func() type1 { initOrder++ assert.Equal(t, 1, initOrder) return type1{} } new2 := func(type1) type2 { initOrder++ assert.Equal(t, 2, initOrder) return type2{} } new3 := func(type1, type2) type3 { initOrder++ assert.Equal(t, 3, initOrder) return type3{} } biz := func(s1 type1, s2 type2, s3 type3) { initOrder++ assert.Equal(t, 4, initOrder) } app := fxtest.New(t, Provide(new1, new2, new3), Invoke(biz), ) defer app.RequireStart().RequireStop() assert.Equal(t, 4, initOrder) }) t.Run("ProvidesCalledLazily", func(t *testing.T) { t.Parallel() count := 0 newBuffer := func() *bytes.Buffer { t.Error("this module should not init: no provided type relies on it") return nil } newEmpty := func() struct{} { count++ return struct{}{} } app := fxtest.New(t, Provide(newBuffer, newEmpty), Invoke(func(struct{}) { count++ }), ) defer app.RequireStart().RequireStop() assert.Equal(t, 2, count) }) t.Run("Error", func(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) New( Provide(&bytes.Buffer{}), // error, not a constructor WithLogger(func() fxevent.Logger { return spy }), ) require.Equal(t, []string{"Provided", "Provided", "Provided", "Provided", "LoggerInitialized"}, spy.EventTypes()) // First 3 provides are Fx types (Lifecycle, Shutdowner, DotGraph). assert.Contains(t, spy.Events()[3].(*fxevent.Provided).Err.Error(), "must provide constructor function") }) } func TestTimeoutOptions(t *testing.T) { t.Parallel() const timeout = time.Minute // Further assertions can't succeed unless the test timeout is greater than the default. require.True(t, timeout > DefaultTimeout, "test assertions require timeout greater than default") var started, stopped bool assertCustomContext := func(ctx context.Context, phase string) { deadline, ok := ctx.Deadline() if assert.True(t, ok, "no %s deadline", phase) { remaining := time.Until(deadline) assert.True(t, remaining > DefaultTimeout, "didn't respect customized %s timeout", phase) } } verify := func(lc Lifecycle) { lc.Append(Hook{ OnStart: func(ctx context.Context) error { assertCustomContext(ctx, "start") started = true return nil }, OnStop: func(ctx context.Context) error { assertCustomContext(ctx, "stop") stopped = true return nil }, }) } app := fxtest.New( t, Invoke(verify), StartTimeout(timeout), StopTimeout(timeout), ) app.RequireStart().RequireStop() assert.True(t, started, "app wasn't started") assert.True(t, stopped, "app wasn't stopped") } func TestAppRunTimeout(t *testing.T) { t.Parallel() // Fails with an error immediately. failure := func() error { return errors.New("great sadness") } // Builds a hook that takes much longer than the application timeout. takeVeryLong := func(clock *fxclock.Mock) func() error { return func() error { // We'll exceed the start and stop timeouts, // and then some. for range 3 { clock.Add(time.Second) } return errors.New("user should not see this") } } tests := []struct { desc string // buildHook builds and returns the hooks for this test case. buildHooks func(*fxclock.Mock) []Hook // Type of the fxevent we want. // Does not reflect the exact value. wantEventType fxevent.Event }{ { // Timeout starting an application. desc: "OnStart timeout", buildHooks: func(clock *fxclock.Mock) []Hook { return []Hook{ StartHook(takeVeryLong(clock)), } }, wantEventType: &fxevent.Started{}, }, { // Timeout during a rollback because start failed. desc: "rollback timeout", buildHooks: func(clock *fxclock.Mock) []Hook { return []Hook{ // The hooks are separate because // OnStop will not be run if that hook failed. StartHook(takeVeryLong(clock)), StopHook(failure), } }, wantEventType: &fxevent.Started{}, }, { // Timeout during a stop. desc: "OnStop timeout", buildHooks: func(clock *fxclock.Mock) []Hook { return []Hook{ StopHook(takeVeryLong(clock)), } }, wantEventType: &fxevent.Stopped{}, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() mockClock := fxclock.NewMock() var ( exitCode int exited bool ) exit := func(code int) { exited = true exitCode = code } defer func() { assert.True(t, exited, "os.Exit must be called") }() // If the OnStart hook for this is invoked, // it means that the Start did not fail. // In that case, shut down immediately // rather than block forever. shutdown := func(sd Shutdowner, lc Lifecycle) { lc.Append(Hook{ OnStart: func(context.Context) error { return sd.Shutdown() }, }) } app, spy := NewSpied( StartTimeout(time.Second), StopTimeout(time.Second), WithExit(exit), WithClock(mockClock), Invoke(func(lc Lifecycle) { hooks := tt.buildHooks(mockClock) for _, h := range hooks { lc.Append(h) } }), Invoke(shutdown), ) app.Run() assert.NotZero(t, exitCode, "exit code mismatch") eventType := reflect.TypeOf(tt.wantEventType).Elem().Name() matchingEvents := spy.Events().SelectByTypeName(eventType) require.Len(t, matchingEvents, 1, "expected a %q event", eventType) event := matchingEvents[0] errv := reflect.ValueOf(event).Elem().FieldByName("Err") require.True(t, errv.IsValid(), "event %q does not have an Err attribute", eventType) err, _ := errv.Interface().(error) assert.ErrorIs(t, err, context.DeadlineExceeded, "should fail because of a timeout") }) } } func TestAppStart(t *testing.T) { t.Parallel() t.Run("Timeout", func(t *testing.T) { t.Parallel() mockClock := fxclock.NewMock() type A struct{} blocker := func(lc Lifecycle) *A { lc.Append( Hook{ OnStart: func(ctx context.Context) error { mockClock.Add(5 * time.Second) return ctx.Err() }, }, ) return &A{} } // NOTE: for tests that gets cancelled/times out during lifecycle methods, it's possible // for them to run into race with fxevent logs getting written to testing.T with the // remainder of the tests. As a workaround, we provide fxlog.Spy to prevent the lifecycle // goroutine from writing to testing.T. spy := new(fxlog.Spy) app := NewForTest(t, WithLogger(func() fxevent.Logger { return spy }), WithClock(mockClock), Provide(blocker), Invoke(func(*A) {}), ) ctx, cancel := mockClock.WithTimeout(context.Background(), time.Second) err := app.Start(ctx) require.Error(t, err) assert.Contains(t, "context deadline exceeded", err.Error()) cancel() }) t.Run("TimeoutWithFinishedHooks", func(t *testing.T) { t.Parallel() mockClock := fxclock.NewMock() type A struct{} type B struct{ A *A } type C struct{ B *B } newA := func(lc Lifecycle) *A { lc.Append( Hook{ OnStart: func(context.Context) error { mockClock.Add(100 * time.Millisecond) return nil }, }, ) return &A{} } newB := func(lc Lifecycle, a *A) *B { lc.Append( Hook{ OnStart: func(context.Context) error { mockClock.Add(300 * time.Millisecond) return nil }, }, ) return &B{a} } newC := func(lc Lifecycle, b *B) *C { lc.Append( Hook{ OnStart: func(ctx context.Context) error { mockClock.Add(5 * time.Second) return ctx.Err() }, }, ) return &C{b} } // NOTE: for tests that gets cancelled/times out during lifecycle methods, it's possible // for them to run into race with fxevent logs getting written to testing.T with the // remainder of the tests. As a workaround, we provide fxlog.Spy to prevent the lifecycle // goroutine from writing to testing.T. spy := new(fxlog.Spy) app := New( WithLogger(func() fxevent.Logger { return spy }), WithClock(mockClock), Provide(newA, newB, newC), Invoke(func(*C) {}), ) ctx, cancel := mockClock.WithTimeout(context.Background(), time.Second) defer cancel() err := app.Start(ctx) require.Error(t, err) assert.Contains(t, "context deadline exceeded", err.Error()) }) t.Run("CtxCancelledDuringStart", func(t *testing.T) { t.Parallel() type A struct{} running := make(chan struct{}) newA := func(lc Lifecycle) *A { lc.Append( Hook{ OnStart: func(ctx context.Context) error { close(running) <-ctx.Done() return ctx.Err() }, }, ) return &A{} } // NOTE: for tests that gets cancelled/times out during lifecycle methods, it's possible // for them to run into race with fxevent logs getting written to testing.T with the // remainder of the tests. As a workaround, we provide fxlog.Spy to prevent the lifecycle // goroutine from writing to testing.T. spy := new(fxlog.Spy) app := New( WithLogger(func() fxevent.Logger { return spy }), Provide(newA), Invoke(func(*A) {}), ) ctx, cancel := context.WithCancel(context.Background()) go func() { <-running cancel() }() err := app.Start(ctx) require.Error(t, err) assert.NotContains(t, err.Error(), "context deadline exceeded") assert.NotContains(t, err.Error(), "timed out while executing hook OnStart") }) t.Run("race test", func(t *testing.T) { t.Parallel() secondStart := make(chan struct{}, 1) firstStop := make(chan struct{}, 1) startReturn := make(chan struct{}, 1) var stop1Run bool app := New( Invoke(func(lc Lifecycle) { lc.Append(Hook{ OnStart: func(context.Context) error { return nil }, OnStop: func(context.Context) error { if stop1Run { require.Fail(t, "Hooks should only run once") } stop1Run = true close(firstStop) <-startReturn return nil }, }) lc.Append(Hook{ OnStart: func(context.Context) error { close(secondStart) <-firstStop return nil }, OnStop: func(context.Context) error { assert.Fail(t, "Stop hook 2 should not be called "+ "if start hook 2 does not finish") return nil }, }) }), ) go func() { assert.NoError(t, app.Start(context.Background())) close(startReturn) }() <-secondStart assert.NoError(t, app.Stop(context.Background())) assert.True(t, stop1Run) }) t.Run("CtxTimeoutDuringStartStillRunsStopHooks", func(t *testing.T) { t.Parallel() var ran bool mockClock := fxclock.NewMock() app := New( WithClock(mockClock), Invoke(func(lc Lifecycle) { lc.Append(Hook{ OnStart: func(ctx context.Context) error { return nil }, OnStop: func(ctx context.Context) error { ran = true return nil }, }) lc.Append(Hook{ OnStart: func(ctx context.Context) error { mockClock.Add(5 * time.Second) return ctx.Err() }, OnStop: func(ctx context.Context) error { assert.Fail(t, "This Stop hook should not be called") return nil }, }) }), ) startCtx, cancelStart := mockClock.WithTimeout(context.Background(), time.Second) defer cancelStart() err := app.Start(startCtx) require.Error(t, err) assert.ErrorContains(t, err, "context deadline exceeded") require.NoError(t, app.Stop(context.Background())) assert.True(t, ran, "Stop hook for the Start hook that finished running should have been called") }) t.Run("Rollback", func(t *testing.T) { t.Parallel() failStart := func(lc Lifecycle) struct{} { lc.Append(Hook{OnStart: func(context.Context) error { return errors.New("OnStart fail") }}) return struct{}{} } app, spy := NewSpied( Provide(failStart), Invoke(func(struct{}) {}), ) err := app.Start(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "OnStart fail") assert.Equal(t, []string{ "Provided", "Provided", "Provided", "Provided", "LoggerInitialized", "Invoking", "BeforeRun", "Run", "BeforeRun", "Run", "Invoked", "OnStartExecuting", "OnStartExecuted", "RollingBack", "RolledBack", "Started", }, spy.EventTypes()) }) t.Run("StartAndStopErrors", func(t *testing.T) { t.Parallel() errStop1 := errors.New("OnStop fail 1") errStart2 := errors.New("OnStart fail 2") fail := func(lc Lifecycle) struct{} { lc.Append(Hook{ OnStart: func(context.Context) error { return nil }, OnStop: func(context.Context) error { return errStop1 }, }) lc.Append(Hook{ OnStart: func(context.Context) error { return errStart2 }, OnStop: func(context.Context) error { assert.Fail(t, "should be never called"); return nil }, }) return struct{}{} } app, spy := NewSpied( Provide(fail), Invoke(func(struct{}) {}), ) err := app.Start(context.Background()) require.Error(t, err) assert.Equal(t, []error{errStart2, errStop1}, multierr.Errors(err)) assert.Equal(t, []string{ "Provided", "Provided", "Provided", "Provided", "LoggerInitialized", "Invoking", "BeforeRun", "Run", "BeforeRun", "Run", "Invoked", "OnStartExecuting", "OnStartExecuted", "OnStartExecuting", "OnStartExecuted", "RollingBack", "OnStopExecuting", "OnStopExecuted", "RolledBack", "Started", }, spy.EventTypes()) }) t.Run("InvokeNonFunction", func(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) app := New(WithLogger(func() fxevent.Logger { return spy }), Invoke(struct{}{})) err := app.Err() require.Error(t, err, "expected start failure") assert.Contains(t, err.Error(), "can't invoke non-function") // Example // fx.Invoke({}) called from: // go.uber.org/fx_test.TestAppStart.func4 // /.../fx/app_test.go:525 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 // Failed: can't invoke non-function {} (type struct {}) require.Equal(t, []string{"Provided", "Provided", "Provided", "LoggerInitialized", "Invoking", "Invoked"}, spy.EventTypes()) failedEvent := spy.Events()[len(spy.EventTypes())-1].(*fxevent.Invoked) assert.Contains(t, failedEvent.Err.Error(), "can't invoke non-function") assert.Contains(t, failedEvent.Trace, "go.uber.org/fx_test.TestAppStart") assert.Contains(t, failedEvent.Trace, "/app_test.go") }) t.Run("ProvidingAProvideShouldFail", func(t *testing.T) { t.Parallel() type type1 struct{} type type2 struct{} type type3 struct{} app := NewForTest(t, Provide( func() type1 { return type1{} }, Provide( func() type2 { return type2{} }, func() type3 { return type3{} }, ), ), ) err := app.Err() require.Error(t, err, "expected start failure") // Example: // fx.Option should be passed to fx.New directly, not to fx.Provide: fx.Provide received fx.Provide(go.uber.org/fx_test.TestAppStart.func5.2(), go.uber.org/fx_test.TestAppStart.func5.3()) from: // go.uber.org/fx_test.TestAppStart.func5 // /.../fx/app_test.go:550 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 assert.Contains(t, err.Error(), "fx.Option should be passed to fx.New directly, not to fx.Provide") assert.Contains(t, err.Error(), "fx.Provide received fx.Provide(go.uber.org/fx_test.TestAppStart") assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestAppStart") assert.Contains(t, err.Error(), "/app_test.go") }) t.Run("InvokingAnInvokeShouldFail", func(t *testing.T) { t.Parallel() type type1 struct{} app := NewForTest(t, Provide(func() type1 { return type1{} }), Invoke(Invoke(func(type1) { })), ) newErr := app.Err() require.Error(t, newErr) err := app.Start(context.Background()) require.Error(t, err, "expected start failure") assert.Equal(t, err, newErr, "start should return the same error fx.New encountered") // Example // fx.Option should be passed to fx.New directly, not to fx.Invoke: fx.Invoke received fx.Invoke(go.uber.org/fx_test.TestAppStart.func6.2()) from: // go.uber.org/fx_test.TestAppStart.func6 // /.../fx/app_test.go:579 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 assert.Contains(t, err.Error(), "fx.Option should be passed to fx.New directly, not to fx.Invoke") assert.Contains(t, err.Error(), "fx.Invoke received fx.Invoke(go.uber.org/fx_test.TestAppStart") assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestAppStart") assert.Contains(t, err.Error(), "/app_test.go") }) t.Run("ProvidingOptionsShouldFail", func(t *testing.T) { t.Parallel() type type1 struct{} type type2 struct{} type type3 struct{} module := Options( Provide( func() type1 { return type1{} }, func() type2 { return type2{} }, ), Invoke(func(type1) { require.FailNow(t, "module Invoked must not be called") }), ) app := NewForTest(t, Provide( func() type3 { return type3{} }, module, ), ) err := app.Err() require.Error(t, err, "expected start failure") // Example: // fx.Annotated should be passed to fx.Provide directly, it should not be returned by the constructor: fx.Provide received go.uber.org/fx_test.TestAnnotatedWrongUsage.func2.1() from: // go.uber.org/fx_test.TestAnnotatedWrongUsage.func2 // /.../fx/annotated_test.go:76 // testing.tRunner // /.../go/1.13.3/libexec/src/testing/testing.go:909 assert.Contains(t, err.Error(), "fx.Option should be passed to fx.New directly, not to fx.Provide") assert.Contains(t, err.Error(), "fx.Provide received fx.Options(fx.Provide(go.uber.org/fx_test.TestAppStart") assert.Contains(t, err.Error(), "go.uber.org/fx_test.TestAppStart") assert.Contains(t, err.Error(), "/app_test.go") }) t.Run("HookGoroutineExitsErrorMsg", func(t *testing.T) { t.Parallel() addHook := func(lc Lifecycle) { lc.Append(Hook{ OnStart: func(ctx context.Context) error { runtime.Goexit() return nil }, }) } app := fxtest.New(t, Invoke(addHook), ) err := app.Start(context.Background()).Error() assert.Contains(t, "goroutine exited without returning", err) }) t.Run("StartTwiceWithHooksErrors", func(t *testing.T) { t.Parallel() ctx := t.Context() app := fxtest.New(t, Invoke(func(lc Lifecycle) { lc.Append(Hook{ OnStart: func(ctx context.Context) error { return nil }, OnStop: func(ctx context.Context) error { return nil }, }) }), ) assert.NoError(t, app.Start(ctx)) err := app.Start(ctx) if assert.Error(t, err) { assert.ErrorContains(t, err, "attempted to start lifecycle when in state: started") } app.Stop(ctx) assert.NoError(t, app.Start(ctx)) app.Stop(ctx) }) } func TestAppStop(t *testing.T) { t.Parallel() t.Run("Timeout", func(t *testing.T) { t.Parallel() mockClock := fxclock.NewMock() block := func(ctx context.Context) error { mockClock.Add(5 * time.Second) return ctx.Err() } // NOTE: for tests that gets cancelled/times out during lifecycle methods, it's possible // for them to run into race with fxevent logs getting written to testing.T with the // remainder of the tests. As a workaround, we provide fxlog.Spy to prevent the lifecycle // goroutine from writing to testing.T. spy := new(fxlog.Spy) app := New(Invoke(func(l Lifecycle) { l.Append(Hook{OnStop: block}) }), WithLogger(func() fxevent.Logger { return spy }), WithClock(mockClock), ) err := app.Start(context.Background()) require.Nil(t, err) ctx, cancel := mockClock.WithTimeout(context.Background(), time.Second) defer cancel() err = app.Stop(ctx) require.Error(t, err) assert.Contains(t, err.Error(), "context deadline exceeded") }) t.Run("StopError", func(t *testing.T) { t.Parallel() failStop := func(lc Lifecycle) struct{} { lc.Append(Hook{OnStop: func(context.Context) error { return errors.New("OnStop fail") }}) return struct{}{} } app := fxtest.New(t, Provide(failStop), Invoke(func(struct{}) {}), ) app.RequireStart() err := app.Stop(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "OnStop fail") }) } func TestValidateApp(t *testing.T) { t.Parallel() // helper to use the test logger validateApp := func(t *testing.T, opts ...Option) error { return ValidateApp( append(opts, Logger(fxtest.NewTestPrinter(t)))..., ) } t.Run("do not run provides on graph validation", func(t *testing.T) { t.Parallel() type type1 struct{} err := validateApp(t, Provide(func() *type1 { t.Error("provide must not be called") return nil }), Invoke(func(*type1) {}), ) require.NoError(t, err) }) t.Run("do not run provides nor invokes on graph validation", func(t *testing.T) { t.Parallel() type type1 struct{} err := validateApp(t, Provide(func() *type1 { t.Error("provide must not be called") return nil }), Invoke(func(*type1) { t.Error("invoke must not be called") }), ) require.NoError(t, err) }) t.Run("provide depends on something not available", func(t *testing.T) { t.Parallel() type type1 struct{} err := validateApp(t, Provide(func(type1) int { return 0 }), Invoke(func(int) error { return nil }), ) require.Error(t, err, "fx.ValidateApp should error on argument not available") errMsg := err.Error() assert.Contains(t, errMsg, "could not build arguments for function") assert.Contains(t, errMsg, "failed to build int: missing dependencies for function") assert.Contains(t, errMsg, "missing type: fx_test.type1") }) t.Run("provide introduces a cycle", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} err := validateApp(t, Provide(func(A) B { return B{} }), Provide(func(B) A { return A{} }), Invoke(func(B) {}), ) require.Error(t, err, "fx.ValidateApp should error on cycle") errMsg := err.Error() assert.Contains(t, errMsg, "cycle detected in dependency graph") }) t.Run("invoke a type that's not available", func(t *testing.T) { t.Parallel() type A struct{} err := validateApp(t, Invoke(func(A) {}), ) require.Error(t, err, "fx.ValidateApp should return an error on missing invoke dep") errMsg := err.Error() assert.Contains(t, errMsg, "missing dependencies for function") assert.Contains(t, errMsg, "missing type: fx_test.A") }) t.Run("no error", func(t *testing.T) { t.Parallel() type A struct{} err := validateApp(t, Provide(func() A { return A{} }), Invoke(func(A) {}), ) require.NoError(t, err, "fx.ValidateApp should not return an error") }) } func TestHookConstructors(t *testing.T) { t.Run("all", func(t *testing.T) { var ( calls = map[string]int{ "start func": 0, "start func err": 0, "start ctx func": 0, "start ctx func err": 0, "stop func": 0, "stop func err": 0, "stop ctx func": 0, "stop ctx func err": 0, } nilFunc func() nilErrorFunc func() error nilContextFunc func(context.Context) nilContextErrorFunc func(context.Context) error ) fxtest.New( t, Invoke(func(lc Lifecycle) { // Nil functions lc.Append(StartStopHook(nilFunc, nilErrorFunc)) lc.Append(StartStopHook(nilContextFunc, nilContextErrorFunc)) // Start hooks lc.Append(StartHook(func() { calls["start func"]++ })) lc.Append(StartHook(func() error { calls["start func err"]++ return nil })) lc.Append(StartHook(func(context.Context) { calls["start ctx func"]++ })) lc.Append(StartHook(func(context.Context) error { calls["start ctx func err"]++ return nil })) // Stop hooks lc.Append(StopHook(func() { calls["stop func"]++ })) lc.Append(StopHook(func() error { calls["stop func err"]++ return nil })) lc.Append(StopHook(func(context.Context) { calls["stop ctx func"]++ })) lc.Append(StopHook(func(context.Context) error { calls["stop ctx func err"]++ return nil })) // StartStop hooks lc.Append(StartStopHook( func() { calls["start func"]++ }, func() error { calls["stop func err"]++ return nil }, )) lc.Append(StartStopHook( func() error { calls["start func err"]++ return nil }, func(context.Context) { calls["stop ctx func"]++ }, )) lc.Append(StartStopHook( func(context.Context) { calls["start ctx func"]++ }, func(context.Context) error { calls["stop ctx func err"]++ return nil }, )) lc.Append(StartStopHook( func(context.Context) error { calls["start ctx func err"]++ return nil }, func() { calls["stop func"]++ }, )) }), ).RequireStart().RequireStop() for name, count := range calls { require.Equalf(t, 2, count, "bad call count: %s (%d)", name, count) } }) t.Run("start errors", func(t *testing.T) { wantErr := errors.New("oh no") // Verify that wrapped `func() error` funcs produce the expected error. err := New(Invoke(func(lc Lifecycle) { lc.Append(StartHook(func() error { return wantErr })) })).Start(context.Background()) require.ErrorContains(t, err, wantErr.Error()) // Verify that wrapped `func(context.Context) error` funcs produce the // expected error. err = New(Invoke(func(lc Lifecycle) { lc.Append(StartHook(func(ctx context.Context) error { return wantErr })) })).Start(context.Background()) require.ErrorContains(t, err, wantErr.Error()) }) t.Run("start deadline", func(t *testing.T) { wantCtx, cancel := context.WithTimeout(context.Background(), 123*time.Second) defer cancel() // Verify that `func(context.Context)` funcs receive the expected // deadline. app := New(Invoke(func(lc Lifecycle) { lc.Append(StartHook(func(ctx context.Context) { var ( want, wantOK = wantCtx.Deadline() give, giveOK = ctx.Deadline() ) require.Equal(t, wantOK, giveOK) require.True(t, want.Equal(give)) })) })) require.NoError(t, app.Start(wantCtx)) require.NoError(t, app.Stop(wantCtx)) // Verify that `func(context.Context) error` funcs receive the expected // deadline. app = New(Invoke(func(lc Lifecycle) { lc.Append(StartHook(func(ctx context.Context) error { var ( want, wantOK = wantCtx.Deadline() give, giveOK = ctx.Deadline() ) require.Equal(t, wantOK, giveOK) require.True(t, want.Equal(give)) return nil })) })) require.NoError(t, app.Start(wantCtx)) require.NoError(t, app.Stop(wantCtx)) }) t.Run("stop errors", func(t *testing.T) { var ( ctx = context.Background() wantErr = errors.New("oh no") ) // Verify that wrapped `func() error` funcs produce the expected error. app := New(Invoke(func(lc Lifecycle) { lc.Append(StopHook(func() error { return wantErr })) })) require.NoError(t, app.Start(ctx)) require.ErrorContains(t, app.Stop(ctx), wantErr.Error()) // Verify that wrapped `func(context.Context) error` funcs produce the // expected error. app = New(Invoke(func(lc Lifecycle) { lc.Append(StopHook(func(ctx context.Context) error { return wantErr })) })) require.NoError(t, app.Start(ctx)) require.ErrorIs(t, app.Stop(ctx), wantErr) }) t.Run("stop deadline", func(t *testing.T) { wantCtx, cancel := context.WithTimeout( context.Background(), 123*time.Second, ) defer cancel() // Verify that `func(context.Context)` funcs receive the expected // deadline. app := New(Invoke(func(lc Lifecycle) { lc.Append(StopHook(func(ctx context.Context) { var ( want, wantOK = wantCtx.Deadline() give, giveOK = ctx.Deadline() ) require.Equal(t, wantOK, giveOK) require.True(t, want.Equal(give)) })) })) require.NoError(t, app.Start(wantCtx)) require.NoError(t, app.Stop(wantCtx)) // Verify that `func(context.Context) error` funcs receive the expected // deadline. app = New(Invoke(func(lc Lifecycle) { lc.Append(StopHook(func(ctx context.Context) error { var ( want, wantOK = wantCtx.Deadline() give, giveOK = ctx.Deadline() ) require.Equal(t, wantOK, giveOK) require.True(t, want.Equal(give)) return nil })) })) require.NoError(t, app.Start(wantCtx)) require.NoError(t, app.Stop(wantCtx)) }) t.Run("typed", func(t *testing.T) { type ( funcType func() funcErrType func() error ctxFuncType func(context.Context) ctxFuncErrType func(context.Context) error ) var ( calls int myFunc = funcType(func() { calls++ }) myFuncErr = funcErrType(func() error { calls++ return nil }) myCtxFunc = ctxFuncType(func(context.Context) { calls++ }) myCtxFuncErr = ctxFuncErrType(func(context.Context) error { calls++ return nil }) ) // Ensure that user function types that are assignable to supported // base function types are converted as expected. require.NotPanics(t, func() { fxtest.New(t, Invoke(func(lc Lifecycle) { lc.Append(StartStopHook(myFunc, myFuncErr)) lc.Append(StartStopHook(myCtxFunc, myCtxFuncErr)) })).RequireStart().RequireStop() }) require.Equal(t, 4, calls) }) } func TestDone(t *testing.T) { t.Parallel() app := fxtest.New(t) defer app.RequireStop() done := app.Done() require.NotNil(t, done, "Got a nil channel.") select { case sig := <-done: t.Fatalf("Got unexpected signal %v from application's Done channel.", sig) default: } } // TestShutdownThenWait tests that if we call .Shutdown before waiting, the wait // will still return the last shutdown signal. func TestShutdownThenWait(t *testing.T) { t.Parallel() var ( s Shutdowner stopped bool ) app := fxtest.New( t, Populate(&s), Invoke(func(lc Lifecycle) { lc.Append(StopHook(func() { stopped = true })) }), ).RequireStart() require.NotNil(t, s) err := s.Shutdown(ExitCode(1337)) assert.NoError(t, err) assert.False(t, stopped) shutdownSig := <-app.Wait() assert.Equal(t, 1337, shutdownSig.ExitCode) assert.False(t, stopped) app.RequireStop() assert.True(t, stopped) } func TestReplaceLogger(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) app := fxtest.New(t, WithLogger(func() fxevent.Logger { return spy })) app.RequireStart().RequireStop() assert.Equal(t, []string{ "Provided", "Provided", "Provided", "LoggerInitialized", "Started", "Stopped", }, spy.EventTypes()) } func TestNopLogger(t *testing.T) { t.Parallel() app := fxtest.New(t, NopLogger) app.RequireStart().RequireStop() } func TestNopLoggerOptionString(t *testing.T) { t.Parallel() assert.Equal(t, "fx.WithLogger(go.uber.org/fx.init.func1())", NopLogger.String(), ) } func TestCustomLoggerWithPrinter(t *testing.T) { t.Parallel() // If we provide both, an fx.Logger and fx.WithLogger, and the logger // fails, we should fall back to the fx.Logger. var buff bytes.Buffer app := New( Logger(log.New(&buff, "", 0)), WithLogger(func() (fxevent.Logger, error) { return nil, errors.New("great sadness") }), ) err := app.Start(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "great sadness") out := buff.String() assert.Contains(t, out, "failed to build fxevent.Logger") assert.Contains(t, out, "great sadness") } func TestCustomLoggerWithLifecycle(t *testing.T) { t.Parallel() var started, stopped bool defer func() { assert.True(t, started, "never started") assert.True(t, stopped, "never stopped") }() var buff bytes.Buffer defer func() { assert.Empty(t, buff.String(), "unexpectedly wrote to the fallback logger") }() var spy fxlog.Spy app := New( // We expect WithLogger to do its job. This means we shouldn't // print anything to this buffer. Logger(log.New(&buff, "", 0)), WithLogger(func(lc Lifecycle) fxevent.Logger { lc.Append(Hook{ OnStart: func(context.Context) error { assert.False(t, started, "started twice") started = true return nil }, OnStop: func(context.Context) error { assert.False(t, stopped, "stopped twice") stopped = true return nil }, }) return &spy }), ) require.NoError(t, app.Start(context.Background())) require.NoError(t, app.Stop(context.Background())) assert.Equal(t, []string{ "Provided", "Provided", "Provided", "BeforeRun", "Run", "LoggerInitialized", "OnStartExecuting", "OnStartExecuted", "Started", "OnStopExecuting", "OnStopExecuted", "Stopped", }, spy.EventTypes()) } func TestCustomLoggerFailure(t *testing.T) { t.Parallel() var buff bytes.Buffer app := New( // We expect WithLogger to fail, so this buffer should be // contain information about the failure. Logger(log.New(&buff, "", 0)), WithLogger(func() (fxevent.Logger, error) { return nil, errors.New("great sadness") }), ) require.Error(t, app.Err()) out := buff.String() assert.Contains(t, out, "Failed to initialize custom logger") assert.Contains(t, out, "failed to build fxevent.Logger") assert.Contains(t, out, "received non-nil error from function") assert.Contains(t, out, "great sadness") } type testErrorWithGraph struct { graph string } func (we testErrorWithGraph) Graph() DotGraph { return DotGraph(we.graph) } func (we testErrorWithGraph) Error() string { return "great sadness" } func TestVisualizeError(t *testing.T) { t.Parallel() t.Run("NotWrappedError", func(t *testing.T) { t.Parallel() _, err := VisualizeError(errors.New("great sadness")) require.Error(t, err) }) t.Run("WrappedErrorWithEmptyGraph", func(t *testing.T) { t.Parallel() graph, err := VisualizeError(testErrorWithGraph{graph: ""}) assert.Empty(t, graph) require.Error(t, err) }) t.Run("WrappedError", func(t *testing.T) { t.Parallel() graph, err := VisualizeError(testErrorWithGraph{graph: "graph"}) assert.Equal(t, "graph", graph) require.NoError(t, err) }) } func TestErrorHook(t *testing.T) { t.Parallel() t.Run("UnvisualizableError", func(t *testing.T) { t.Parallel() type A struct{} var graphErr error h := errHandlerFunc(func(err error) { _, graphErr = VisualizeError(err) }) NewForTest(t, Provide(func() A { return A{} }), Invoke(func(A) error { return errors.New("great sadness") }), ErrorHook(h), ) assert.Equal(t, errors.New("unable to visualize error"), graphErr) }) t.Run("GraphWithError", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} var errStr, graphStr string h := errHandlerFunc(func(err error) { errStr = err.Error() graphStr, _ = VisualizeError(err) }) NewForTest(t, Provide(func() (B, error) { return B{}, fmt.Errorf("great sadness") }), Provide(func(B) A { return A{} }), Invoke(func(A) {}), ErrorHook(&h), ) assert.Contains(t, errStr, "great sadness") assert.Contains(t, graphStr, `"fx_test.B" [color=red];`) assert.Contains(t, graphStr, `"fx_test.A" [color=orange];`) }) t.Run("GraphWithErrorInModule", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} var errStr, graphStr string h := errHandlerFunc(func(err error) { errStr = err.Error() graphStr, _ = VisualizeError(err) }) NewForTest(t, Module("module", Provide(func() (B, error) { return B{}, fmt.Errorf("great sadness") }), Provide(func(B) A { return A{} }), Invoke(func(A) {}), ErrorHook(&h), ), ) assert.Contains(t, errStr, "great sadness") assert.Contains(t, graphStr, `"fx_test.B" [color=red];`) assert.Contains(t, graphStr, `"fx_test.A" [color=orange];`) }) } func TestOptionString(t *testing.T) { t.Parallel() tests := []struct { desc string give Option want string }{ { desc: "Provide", give: Provide(bytes.NewReader), want: "fx.Provide(bytes.NewReader())", }, { desc: "Invoked", give: Invoke(func(c io.Closer) error { return c.Close() }), want: "fx.Invoke(go.uber.org/fx_test.TestOptionString.func1())", }, { desc: "Error/single", give: Error(errors.New("great sadness")), want: "fx.Error(great sadness)", }, { desc: "Error/multiple", give: Error(errors.New("foo"), errors.New("bar")), want: "fx.Error(foo; bar)", }, { desc: "Options/single", give: Options(Provide(bytes.NewBuffer)), // NOTE: We don't prune away fx.Options for the empty // case because we want to attach additional // information to the fx.Options object in the future. want: "fx.Options(fx.Provide(bytes.NewBuffer()))", }, { desc: "Options/multiple", give: Options( Provide(bytes.NewBufferString), Invoke(func(buf *bytes.Buffer) { buf.WriteString("hello") }), ), want: "fx.Options(" + "fx.Provide(bytes.NewBufferString()), " + "fx.Invoke(go.uber.org/fx_test.TestOptionString.func2())" + ")", }, { desc: "StartTimeout", give: StartTimeout(time.Second), want: "fx.StartTimeout(1s)", }, { desc: "StopTimeout", give: StopTimeout(5 * time.Second), want: "fx.StopTimeout(5s)", }, { desc: "RecoverFromPanics", give: RecoverFromPanics(), want: "fx.RecoverFromPanics()", }, { desc: "Logger", give: WithLogger(func() fxevent.Logger { return testLogger{t} }), want: "fx.WithLogger(go.uber.org/fx_test.TestOptionString.func3())", }, { desc: "ErrorHook", give: ErrorHook(testErrorHandler{t}), want: "fx.ErrorHook(TestOptionString)", }, { desc: "Supplied/simple", give: Supply(bytes.NewReader(nil), bytes.NewBuffer(nil)), want: "fx.Supply(*bytes.Reader, *bytes.Buffer)", }, { desc: "Supplied/Annotated", give: Supply(Annotated{Target: bytes.NewReader(nil)}), want: "fx.Supply(*bytes.Reader)", }, { desc: "Decorate", give: Decorate(bytes.NewBufferString), want: "fx.Decorate(bytes.NewBufferString())", }, { desc: "Replace", give: Replace(bytes.NewReader(nil)), want: "fx.Replace(*bytes.Reader)", }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() stringer, ok := tt.give.(fmt.Stringer) require.True(t, ok, "option must implement stringer") assert.Equal(t, tt.want, stringer.String()) }) } } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } type testLogger struct{ t *testing.T } func (l testLogger) Printf(s string, args ...any) { l.t.Logf(s, args...) } func (l testLogger) LogEvent(event fxevent.Event) { l.t.Logf("emitted event %#v", event) } func (l testLogger) String() string { return l.t.Name() } type testErrorHandler struct{ t *testing.T } func (h testErrorHandler) HandleError(err error) { h.t.Error(err) } func (h testErrorHandler) String() string { return h.t.Name() } ================================================ FILE: app_unixes.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package fx import "golang.org/x/sys/unix" const ( _sigINT = unix.SIGINT _sigTERM = unix.SIGTERM ) ================================================ FILE: app_wasm.go ================================================ // Copyright (c) 2023 Uber Technologies, Inc. // // 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:build (js && wasm) || (wasip1 && wasm) // +build js,wasm wasip1,wasm package fx import "syscall" const ( _sigINT = syscall.SIGINT _sigTERM = syscall.SIGTERM ) ================================================ FILE: app_windows.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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:build windows // +build windows package fx import "golang.org/x/sys/windows" const ( _sigINT = windows.SIGINT _sigTERM = windows.SIGTERM ) ================================================ FILE: app_windows_test.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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:build windows // +build windows package fx_test import ( "context" "fmt" "io" "os" "os/exec" "syscall" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "golang.org/x/sys/windows" ) // Regression test for https://github.com/uber-go/fx/issues/781. func TestWindowsCtrlCHandler(t *testing.T) { // This test operates by launching a separate process, // which we'll send a SIGINT to, // and verifying the output of the application. // Launch a separate process we will send SIGINT to. testExe, err := os.Executable() require.NoError(t, err, "determine test executable") cmd := exec.Command(testExe, "-test.run", "TestWindowsMinimalApp") cmd.Env = append(os.Environ(), "FX_TEST_FAKE=1") // On Windows, we need to use GenerateConsoleCtrlEvent // to SIGINT the child process. // // That API operates on Group ID granularity, // so we need to make sure our new child process // gets a new group ID rather than using the same ID // as the test we're running. cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: windows.CREATE_NEW_PROCESS_GROUP, } stdout, err := cmd.StdoutPipe() require.NoError(t, err, "create stdout") stderr, err := cmd.StderrPipe() require.NoError(t, err, "create stderr") require.NoError(t, cmd.Start()) // Block until the child is ready by waiting for the "ready" text // printed to stderr. ready := make(chan struct{}) go func() { defer close(ready) stderr.Read(make([]byte, 1024)) }() <-ready require.NoError(t, windows.GenerateConsoleCtrlEvent(1, uint32(cmd.Process.Pid)), "SIGINT child process") // Drain stdout and stderr, and wait for the process to exit. output, err := io.ReadAll(stdout) require.NoError(t, err) _, err = io.Copy(io.Discard, stderr) require.NoError(t, err) require.NoError(t, cmd.Wait()) assert.Contains(t, string(output), "ONSTOP", "stdout should include ONSTOP") } func TestWindowsMinimalApp(t *testing.T) { // This is not a real test. // It defines the behavior of the fake application // that we spawn from TestWindowsCtrlCHandler. if os.Getenv("FX_TEST_FAKE") != "1" { return } // An Fx application that prints "ready" to stderr // once its start hooks have been invoked, // and "ONSTOP" to stdout when its stop hooks have been invoked. fx.New( fx.NopLogger, fx.Invoke(func(lifecycle fx.Lifecycle) { lifecycle.Append(fx.Hook{ OnStart: func(ctx context.Context) error { fmt.Fprintln(os.Stderr, "ready") return nil }, OnStop: func(ctx context.Context) error { fmt.Fprintln(os.Stdout, "ONSTOP") return nil }, }) }), ).Run() } ================================================ FILE: broadcast.go ================================================ // Copyright (c) 2024 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "os" "sync" ) // broadcaster broadcasts signals to registered signal listeners. // All methods on the broadcaster are concurrency-safe. type broadcaster struct { // This lock is used to protect all fields of broadcaster. // Methods on broadcaster should protect all concurrent access // by taking this lock when accessing its fields. // Conversely, this lock should NOT be taken outside of broadcaster. m sync.Mutex // last will contain a pointer to the last ShutdownSignal received, or // nil if none, if a new channel is created by Wait or Done, this last // signal will be immediately written to, this allows Wait or Done state // to be read after application stop last *ShutdownSignal // contains channels created by Done done []chan os.Signal // contains channels created by Wait wait []chan ShutdownSignal } func (b *broadcaster) reset() { b.m.Lock() defer b.m.Unlock() b.last = nil } // Done creates a new channel that will receive signals being broadcast // via the broadcaster. // // If a signal has been received prior to the call of Done, // the signal will be sent to the new channel. func (b *broadcaster) Done() <-chan os.Signal { b.m.Lock() defer b.m.Unlock() ch := make(chan os.Signal, 1) // If we had received a signal prior to the call of done, send it's // os.Signal to the new channel. // However we still want to have the operating system notify signals to this // channel should the application receive another. if b.last != nil { ch <- b.last.Signal } b.done = append(b.done, ch) return ch } // Wait creates a new channel that will receive signals being broadcast // via the broadcaster. // // If a signal has been received prior to the call of Wait, // the signal will be sent to the new channel. func (b *broadcaster) Wait() <-chan ShutdownSignal { b.m.Lock() defer b.m.Unlock() ch := make(chan ShutdownSignal, 1) if b.last != nil { ch <- *b.last } b.wait = append(b.wait, ch) return ch } // Broadcast sends the given signal to all channels that have been created // via Done or Wait. It does not block on sending, and returns an unsentSignalError // if any send did not go through. func (b *broadcaster) Broadcast(signal ShutdownSignal) error { b.m.Lock() defer b.m.Unlock() b.last = &signal channels, unsent := b.broadcast( signal, b.broadcastDone, b.broadcastWait, ) if unsent != 0 { return &unsentSignalError{ Signal: signal, Total: channels, Unsent: unsent, } } return nil } func (b *broadcaster) broadcast( signal ShutdownSignal, anchors ...func(ShutdownSignal) (int, int), ) (int, int) { var channels, unsent int for _, anchor := range anchors { c, u := anchor(signal) channels += c unsent += u } return channels, unsent } func (b *broadcaster) broadcastDone(signal ShutdownSignal) (int, int) { var unsent int for _, reader := range b.done { select { case reader <- signal.Signal: default: unsent++ } } return len(b.done), unsent } func (b *broadcaster) broadcastWait(signal ShutdownSignal) (int, int) { var unsent int for _, reader := range b.wait { select { case reader <- signal: default: unsent++ } } return len(b.wait), unsent } type unsentSignalError struct { Signal ShutdownSignal Unsent int Total int } func (err *unsentSignalError) Error() string { return fmt.Sprintf( "send %v signal: %v/%v channels are blocked", err.Signal, err.Unsent, err.Total, ) } ================================================ FILE: decorate.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "reflect" "strings" "go.uber.org/dig" "go.uber.org/fx/internal/fxreflect" ) // Decorate specifies one or more decorator functions to an Fx application. // // # Decorator functions // // Decorator functions let users augment objects in the graph. // They can take in zero or more dependencies that must be provided to the // application with fx.Provide, and produce one or more values that can be used // by other fx.Provide and fx.Invoke calls. // // fx.Decorate(func(log *zap.Logger) *zap.Logger { // return log.Named("myapp") // }) // fx.Invoke(func(log *zap.Logger) { // log.Info("hello") // // Output: // // {"level": "info","logger":"myapp","msg":"hello"} // }) // // The following decorator accepts multiple dependencies from the graph, // augments and returns one of them. // // fx.Decorate(func(log *zap.Logger, cfg *Config) *zap.Logger { // return log.Named(cfg.Name) // }) // // Similar to fx.Provide, functions passed to fx.Decorate may optionally return // an error as their last result. // If a decorator returns a non-nil error, it will halt application startup. // // fx.Decorate(func(conn *sql.DB, cfg *Config) (*sql.DB, error) { // if err := conn.Ping(); err != nil { // return sql.Open("driver-name", cfg.FallbackDB) // } // return conn, nil // }) // // Decorators support both, fx.In and fx.Out structs, similar to fx.Provide and // fx.Invoke. // // type Params struct { // fx.In // // Client usersvc.Client `name:"readOnly"` // } // // type Result struct { // fx.Out // // Client usersvc.Client `name:"readOnly"` // } // // fx.Decorate(func(p Params) Result { // ... // }) // // Decorators can be annotated with the fx.Annotate function, but not with the // fx.Annotated type. Refer to documentation on fx.Annotate() to learn how to // use it for annotating functions. // // fx.Decorate( // fx.Annotate( // func(client usersvc.Client) usersvc.Client { // // ... // }, // fx.ParamTags(`name:"readOnly"`), // fx.ResultTags(`name:"readOnly"`), // ), // ) // // Decorators support augmenting, filtering, or replacing value groups. // To decorate a value group, expect the entire value group slice and produce // the new slice. // // type HandlerParam struct { // fx.In // // Log *zap.Logger // Handlers []Handler `group:"server" // } // // type HandlerResult struct { // fx.Out // // Handlers []Handler `group:"server" // } // // fx.Decorate(func(p HandlerParam) HandlerResult { // var r HandlerResult // for _, handler := range p.Handlers { // r.Handlers = append(r.Handlers, wrapWithLogger(p.Log, handler)) // } // return r // }), // // Decorators can not add new values to the graph, // only modify or replace existing ones. // Types returned by a decorator that are not already in the graph // will be ignored. // // # Decorator scope // // Modifications made to the Fx graph with fx.Decorate are scoped to the // deepest fx.Module inside which the decorator was specified. // // fx.Module("mymodule", // fx.Decorate(func(log *zap.Logger) *zap.Logger { // return log.Named("myapp") // }), // fx.Invoke(func(log *zap.Logger) { // log.Info("decorated logger") // // Output: // // {"level": "info","logger":"myapp","msg":"decorated logger"} // }), // ), // fx.Invoke(func(log *zap.Logger) { // log.Info("plain logger") // // Output: // // {"level": "info","msg":"plain logger"} // }), // // Decorations specified in the top-level fx.New call apply across the // application and chain with module-specific decorators. // // fx.New( // // ... // fx.Decorate(func(log *zap.Logger) *zap.Logger { // return log.With(zap.Field("service", "myservice")) // }), // // ... // fx.Invoke(func(log *zap.Logger) { // log.Info("outer decorator") // // Output: // // {"level": "info","service":"myservice","msg":"outer decorator"} // }), // // ... // fx.Module("mymodule", // fx.Decorate(func(log *zap.Logger) *zap.Logger { // return log.Named("myapp") // }), // fx.Invoke(func(log *zap.Logger) { // log.Info("inner decorator") // // Output: // // {"level": "info","logger":"myapp","service":"myservice","msg":"inner decorator"} // }), // ), // ) func Decorate(decorators ...any) Option { return decorateOption{ Targets: decorators, Stack: fxreflect.CallerStack(1, 0), } } type decorateOption struct { Targets []any Stack fxreflect.Stack } func (o decorateOption) apply(mod *module) { for _, target := range o.Targets { mod.decorators = append(mod.decorators, decorator{ Target: target, Stack: o.Stack, }) } } func (o decorateOption) String() string { items := make([]string, len(o.Targets)) for i, f := range o.Targets { items[i] = fxreflect.FuncName(f) } return fmt.Sprintf("fx.Decorate(%s)", strings.Join(items, ", ")) } // decorator is a single decorator used in Fx. type decorator struct { // Decorator provided to Fx. Target any // Stack trace of where this provide was made. Stack fxreflect.Stack // Whether this decorator was specified via fx.Replace IsReplace bool ReplaceType reflect.Type // set only if IsReplace } func runDecorator(c container, d decorator, opts ...dig.DecorateOption) (err error) { decorator := d.Target defer func() { if err != nil { err = fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %w", decorator, d.Stack, err) } }() switch decorator := decorator.(type) { case annotated: if dcor, derr := decorator.Build(); derr == nil { err = c.Decorate(dcor, opts...) } default: err = c.Decorate(decorator, opts...) } return } ================================================ FILE: decorate_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx_test import ( "errors" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestDecorateSuccess(t *testing.T) { type Logger struct { Name string } t.Run("objects provided by other modules are decorated", func(t *testing.T) { redis := fx.Module("redis", fx.Provide(func() *Logger { return &Logger{Name: "redis"} }), ) testRedis := fx.Module("testRedis", redis, fx.Decorate(func() *Logger { return &Logger{Name: "testRedis"} }), fx.Invoke(func(l *Logger) { assert.Equal(t, "testRedis", l.Name) }), ) app := fxtest.New(t, testRedis, fx.Invoke(func(l *Logger) { assert.Equal(t, "redis", l.Name) }), ) defer app.RequireStart().RequireStop() }) t.Run("objects in child modules are decorated.", func(t *testing.T) { redis := fx.Module("redis", fx.Decorate(func() *Logger { return &Logger{Name: "redis"} }), fx.Invoke(func(l *Logger) { assert.Equal(t, "redis", l.Name) }), ) app := fxtest.New(t, redis, fx.Provide(func() *Logger { assert.Fail(t, "should not run this") return &Logger{Name: "root"} }), ) defer app.RequireStart().RequireStop() }) t.Run("root decoration applies to all modules", func(t *testing.T) { redis := fx.Module("redis", fx.Invoke(func(l *Logger) { assert.Equal(t, "decorated logger", l.Name) }), ) logger := fx.Module("logger", fx.Provide(func() *Logger { return &Logger{Name: "logger"} }), ) app := fxtest.New(t, redis, logger, fx.Decorate(func(l *Logger) *Logger { return &Logger{Name: "decorated " + l.Name} }), ) defer app.RequireStart().RequireStop() }) t.Run("use Decorate with Annotate", func(t *testing.T) { type Coffee struct { Name string Price int } cafe := fx.Module("cafe", fx.Provide(fx.Annotate(func() *Coffee { return &Coffee{Name: "Americano", Price: 3} }, fx.ResultTags(`group:"coffee"`))), fx.Provide(fx.Annotate(func() *Coffee { return &Coffee{Name: "Cappucino", Price: 4} }, fx.ResultTags(`group:"coffee"`))), fx.Provide(fx.Annotate(func() *Coffee { return &Coffee{Name: "Cold Brew", Price: 4} }, fx.ResultTags(`group:"coffee"`))), ) takeout := fx.Module("takeout", cafe, fx.Decorate(fx.Annotate(func(coffee []*Coffee) []*Coffee { var newC []*Coffee for _, c := range coffee { newC = append(newC, &Coffee{ Name: c.Name, Price: c.Price + 1, }) } return newC }, fx.ParamTags(`group:"coffee"`), fx.ResultTags(`group:"coffee"`))), fx.Invoke(fx.Annotate(func(coffee []*Coffee) { assert.Equal(t, 3, len(coffee)) totalPrice := 0 for _, c := range coffee { totalPrice += c.Price } assert.Equal(t, 4+5+5, totalPrice) }, fx.ParamTags(`group:"coffee"`))), ) app := fxtest.New(t, takeout, ) defer app.RequireStart().RequireStop() }) t.Run("use Decorate with parameter/result struct", func(t *testing.T) { type Logger struct { Name string } type A struct { fx.In Log *Logger Version int `name:"versionNum"` } type B struct { fx.Out Log *Logger Version int `name:"versionNum"` } app := fxtest.New(t, fx.Provide( fx.Annotate(func() int { return 1 }, fx.ResultTags(`name:"versionNum"`)), func() *Logger { return &Logger{Name: "logger"} }, ), fx.Decorate(func(a A) B { return B{ Log: &Logger{Name: a.Log.Name + " decorated"}, Version: a.Version + 1, } }), fx.Invoke(fx.Annotate(func(l *Logger, ver int) { assert.Equal(t, "logger decorated", l.Name) assert.Equal(t, 2, ver) }, fx.ParamTags(``, `name:"versionNum"`))), ) defer app.RequireStart().RequireStop() }) t.Run("decorator with soft value group", func(t *testing.T) { app := fxtest.New(t, fx.Provide( fx.Annotate( func() (string, int) { return "cheeseburger", 15 }, fx.ResultTags(`group:"burger"`, `group:"potato"`), ), ), fx.Provide( fx.Annotate( func() (string, int) { return "mushroomburger", 35 }, fx.ResultTags(`group:"burger"`, `group:"potato"`), ), ), fx.Provide( fx.Annotate( func() string { require.FailNow(t, "should not be called") return "veggieburger" }, fx.ResultTags(`group:"burger"`, `group:"potato"`), ), ), fx.Decorate( fx.Annotate( func(burgers []string) []string { retBurg := make([]string, len(burgers)) for i, burger := range burgers { retBurg[i] = strings.ToUpper(burger) } return retBurg }, fx.ParamTags(`group:"burger,soft"`), fx.ResultTags(`group:"burger"`), ), ), fx.Invoke( fx.Annotate( func(burgers []string, fries []int) { assert.ElementsMatch(t, []string{"CHEESEBURGER", "MUSHROOMBURGER"}, burgers) }, fx.ParamTags(`group:"burger,soft"`, `group:"potato"`), ), ), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("decorator with optional parameter", func(t *testing.T) { type Config struct { Name string } type Logger struct { Name string } type DecoratorParam struct { fx.In Cfg *Config `optional:"true"` Log *Logger } app := fxtest.New(t, fx.Provide(func() *Logger { return &Logger{Name: "log"} }), fx.Decorate(func(p DecoratorParam) *Logger { if p.Cfg != nil { return &Logger{Name: p.Cfg.Name} } return &Logger{Name: p.Log.Name} }), fx.Invoke(func(l *Logger) { assert.Equal(t, l.Name, "log") }), ) defer app.RequireStart().RequireStop() }) t.Run("transitive decoration", func(t *testing.T) { type Config struct { Scope string } type Logger struct { Cfg *Config } app := fxtest.New(t, fx.Provide(func() *Config { return &Config{Scope: "root"} }), fx.Module("child", fx.Decorate(func() *Config { return &Config{Scope: "child"} }), fx.Provide(func(cfg *Config) *Logger { return &Logger{Cfg: cfg} }), fx.Invoke(func(l *Logger) { assert.Equal(t, "child", l.Cfg.Scope) }), ), ) defer app.RequireStart().RequireStop() }) t.Run("ineffective transitive decoration", func(t *testing.T) { type Config struct { Scope string } type Logger struct { Cfg *Config } app := fxtest.New(t, fx.Provide(func() *Config { return &Config{Scope: "root"} }), fx.Provide(func(cfg *Config) *Logger { return &Logger{Cfg: &Config{ Scope: cfg.Scope + " logger", }} }), fx.Module("child", fx.Decorate(func() *Config { return &Config{Scope: "child"} }), // Logger does not get replaced since it was provided // from a different Scope. fx.Invoke(func(l *Logger) { assert.Equal(t, "root logger", l.Cfg.Scope) }), ), ) defer app.RequireStart().RequireStop() }) t.Run("decoration must execute when required by a member of group", func(t *testing.T) { type Drinks any type Coffee struct { Type string Name string Price int } type PriceService struct { DefaultPrice int } app := fxtest.New(t, fx.Provide(func() *PriceService { return &PriceService{DefaultPrice: 3} }), fx.Decorate(func(service *PriceService) *PriceService { service.DefaultPrice = 10 return service }), fx.Provide(fx.Annotate(func(service *PriceService) Drinks { assert.Equal(t, 10, service.DefaultPrice) return &Coffee{Type: "coffee", Name: "Americano", Price: service.DefaultPrice} }, fx.ResultTags(`group:"drinks"`))), fx.Provide(fx.Annotated{Group: "drinks", Target: func() Drinks { return &Coffee{Type: "coffee", Name: "Cold Brew", Price: 4} }}), fx.Invoke(fx.Annotate(func(drinks []Drinks) { assert.Len(t, drinks, 2) }, fx.ParamTags(`group:"drinks"`))), ) defer app.RequireStart().RequireStop() }) } func TestDecorateFailure(t *testing.T) { t.Run("decorator returns an error", func(t *testing.T) { type Logger struct { Name string } app := NewForTest(t, fx.Provide(func() *Logger { return &Logger{Name: "root"} }), fx.Decorate(func(l *Logger) (*Logger, error) { return &Logger{Name: l.Name + "decorated"}, errors.New("minor sadness") }), fx.Invoke(func(l *Logger) { assert.Fail(t, "this should not be executed") }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "minor sadness") }) t.Run("decorator in a nested module returns an error", func(t *testing.T) { type Logger struct { Name string } app := NewForTest(t, fx.Provide(func() *Logger { return &Logger{Name: "root"} }), fx.Module("child", fx.Decorate(func(l *Logger) *Logger { return &Logger{Name: l.Name + "decorated"} }), fx.Decorate(func(l *Logger) *Logger { return &Logger{Name: l.Name + "decorated"} }), fx.Invoke(func(l *Logger) { assert.Fail(t, "this should not be executed") }), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "*fx_test.Logger already decorated") }) t.Run("decorating a type more than once in the same Module errors", func(t *testing.T) { type Logger struct { Name string } app := NewForTest(t, fx.Provide(func() *Logger { return &Logger{Name: "root"} }), fx.Decorate(func(l *Logger) *Logger { return &Logger{Name: "dec1 " + l.Name} }), fx.Decorate(func(l *Logger) *Logger { return &Logger{Name: "dec2 " + l.Name} }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "*fx_test.Logger already decorated") }) t.Run("annotated decorator returns an error", func(t *testing.T) { type Logger struct { Name string } tag := `name:"decoratedLogger"` app := NewForTest(t, fx.Provide(fx.Annotate(func() *Logger { return &Logger{Name: "root"} }, fx.ResultTags(tag))), fx.Decorate(fx.Annotate(func(l *Logger) (*Logger, error) { return &Logger{Name: "dec1 " + l.Name}, errors.New("major sadness") }, fx.ParamTags(tag), fx.ResultTags(tag))), fx.Invoke(fx.Annotate(func(l *Logger) { assert.Fail(t, "this should never run") }, fx.ParamTags(tag))), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "major sadness") }) t.Run("all decorator dependencies must be provided", func(t *testing.T) { type Logger struct { Name string } type Config struct { Name string } app := NewForTest(t, fx.Provide(func() *Logger { return &Logger{Name: "logger"} }), fx.Decorate(func(l *Logger, c *Config) *Logger { return &Logger{Name: l.Name + c.Name} }), fx.Invoke(func(l *Logger) { assert.Fail(t, "this should never run") }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "missing dependencies") }) t.Run("decorate cannot provide a non-existent type", func(t *testing.T) { type Logger struct { Name string } app := NewForTest(t, fx.Decorate(func() *Logger { return &Logger{Name: "decorator"} }), fx.Invoke(func(l *Logger) { assert.Fail(t, "this should never run") }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "missing dependencies") }) } ================================================ FILE: doc.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. // Package fx is a framework that makes it easy to build applications out of // reusable, composable modules. // // Fx applications use dependency injection to eliminate globals without the // tedium of manually wiring together function calls. Unlike other approaches // to dependency injection, Fx works with plain Go functions: you don't need // to use struct tags or embed special types, so Fx automatically works well // with most Go packages. // // # Basic usage // // Basic usage is explained in the package-level example. // If you're new to Fx, start there! // // Advanced features, including named instances, optional parameters, // and value groups, are explained in this section further down. // // # Testing Fx Applications // // To test functions that use the Lifecycle type or to write end-to-end tests // of your Fx application, use the helper functions and types provided by the // go.uber.org/fx/fxtest package. // // # Parameter Structs // // Fx constructors declare their dependencies as function parameters. This can // quickly become unreadable if the constructor has a lot of dependencies. // // func NewHandler(users *UserGateway, comments *CommentGateway, posts *PostGateway, votes *VoteGateway, authz *AuthZGateway) *Handler { // // ... // } // // To improve the readability of constructors like this, create a struct that // lists all the dependencies as fields and change the function to accept that // struct instead. The new struct is called a parameter struct. // // Fx has first class support for parameter structs: any struct embedding // fx.In gets treated as a parameter struct, so the individual fields in the // struct are supplied via dependency injection. Using a parameter struct, we // can make the constructor above much more readable: // // type HandlerParams struct { // fx.In // // Users *UserGateway // Comments *CommentGateway // Posts *PostGateway // Votes *VoteGateway // AuthZ *AuthZGateway // } // // func NewHandler(p HandlerParams) *Handler { // // ... // } // // Though it's rarely necessary to mix the two, constructors can receive any // combination of parameter structs and parameters. // // func NewHandler(p HandlerParams, l *log.Logger) *Handler { // // ... // } // // # Result Structs // // Result structs are the inverse of parameter structs. // These structs represent multiple outputs from a // single function as fields. Fx treats all structs embedding fx.Out as result // structs, so other constructors can rely on the result struct's fields // directly. // // Without result structs, we sometimes have function definitions like this: // // func SetupGateways(conn *sql.DB) (*UserGateway, *CommentGateway, *PostGateway, error) { // // ... // } // // With result structs, we can make this both more readable and easier to // modify in the future: // // type Gateways struct { // fx.Out // // Users *UserGateway // Comments *CommentGateway // Posts *PostGateway // } // // func SetupGateways(conn *sql.DB) (Gateways, error) { // // ... // } // // # Named Values // // Some use cases require the application container to hold multiple values of // the same type. // // A constructor that produces a result struct can tag any field with // `name:".."` to have the corresponding value added to the graph under the // specified name. An application may contain at most one unnamed value of a // given type, but may contain any number of named values of the same type. // // type ConnectionResult struct { // fx.Out // // ReadWrite *sql.DB `name:"rw"` // ReadOnly *sql.DB `name:"ro"` // } // // func ConnectToDatabase(...) (ConnectionResult, error) { // // ... // return ConnectionResult{ReadWrite: rw, ReadOnly: ro}, nil // } // // Similarly, a constructor that accepts a parameter struct can tag any field // with `name:".."` to have the corresponding value injected by name. // // type GatewayParams struct { // fx.In // // WriteToConn *sql.DB `name:"rw"` // ReadFromConn *sql.DB `name:"ro"` // } // // func NewCommentGateway(p GatewayParams) (*CommentGateway, error) { // // ... // } // // Note that both the name AND type of the fields on the // parameter struct must match the corresponding result struct. // // # Optional Dependencies // // Constructors often have optional dependencies on some types: if those types are // missing, they can operate in a degraded state. Fx supports optional // dependencies via the `optional:"true"` tag to fields on parameter structs. // // type UserGatewayParams struct { // fx.In // // Conn *sql.DB // Cache *redis.Client `optional:"true"` // } // // If an optional field isn't available in the container, the constructor // receives the field's zero value. // // func NewUserGateway(p UserGatewayParams, log *log.Logger) (*UserGateway, error) { // if p.Cache == nil { // log.Print("Caching disabled") // } // // ... // } // // Constructors that declare optional dependencies MUST gracefully handle // situations in which those dependencies are absent. // // The optional tag also allows adding new dependencies without breaking // existing consumers of the constructor. // // The optional tag may be combined with the name tag to declare a named // value dependency optional. // // type GatewayParams struct { // fx.In // // WriteToConn *sql.DB `name:"rw"` // ReadFromConn *sql.DB `name:"ro" optional:"true"` // } // // func NewCommentGateway(p GatewayParams, log *log.Logger) (*CommentGateway, error) { // if p.ReadFromConn == nil { // log.Print("Warning: Using RW connection for reads") // p.ReadFromConn = p.WriteToConn // } // // ... // } // // # Value Groups // // To make it easier to produce and consume many values of the same type, Fx // supports named, unordered collections called value groups. // // Constructors can send values into value groups by returning a result struct // tagged with `group:".."`. // // type HandlerResult struct { // fx.Out // // Handler Handler `group:"server"` // } // // func NewHelloHandler() HandlerResult { // // ... // } // // func NewEchoHandler() HandlerResult { // // ... // } // // Any number of constructors may provide values to this named collection, but // the ordering of the final collection is unspecified. // // Value groups require parameter and result structs to use fields with // different types: if a group of constructors each returns type T, parameter // structs consuming the group must use a field of type []T. // // Parameter structs can request a value group by using a field of type []T // tagged with `group:".."`. // This will execute all constructors that provide a value to // that group in an unspecified order, then collect all the results into a // single slice. // // type ServerParams struct { // fx.In // // Handlers []Handler `group:"server"` // } // // func NewServer(p ServerParams) *Server { // server := newServer() // for _, h := range p.Handlers { // server.Register(h) // } // return server // } // // Note that values in a value group are unordered. Fx makes no guarantees // about the order in which these values will be produced. // // # Soft Value Groups // // By default, when a constructor declares a dependency on a value group, // all values provided to that value group are eagerly instantiated. // That is undesirable for cases where an optional component wants to // contribute to a value group, but only if it was actually used // by the rest of the application. // // A soft value group can be thought of as a best-attempt at populating the // group with values from constructors that have already run. In other words, // if a constructor's output type is only consumed by a soft value group, // it will not be run. // // Note that Fx randomizes the order of values in the value group, // so the slice of values may not match the order in which constructors // were run. // // To declare a soft relationship between a group and its constructors, use // the `soft` option on the input group tag (`group:"[groupname],soft"`). // This option is only valid for input parameters. // // type Params struct { // fx.In // // Handlers []Handler `group:"server,soft"` // Logger *zap.Logger // } // // func NewServer(p Params) *Server { // // ... // } // // With such a declaration, a constructor that provides a value to the 'server' // value group will be called only if there's another instantiated component // that consumes the results of that constructor. // // func NewHandlerAndLogger() (Handler, *zap.Logger) { // // ... // } // // func NewHandler() Handler { // // ... // } // // fx.Provide( // fx.Annotate(NewHandlerAndLogger, fx.ResultTags(`group:"server"`)), // fx.Annotate(NewHandler, fx.ResultTags(`group:"server"`)), // ) // // NewHandlerAndLogger will be called because the Logger is consumed by the // application, but NewHandler will not be called because it's only consumed // by the soft value group. // // # Value group flattening // // By default, values of type T produced to a value group are consumed as []T. // // type HandlerResult struct { // fx.Out // // Handler Handler `group:"server"` // } // // type ServerParams struct { // fx.In // // Handlers []Handler `group:"server"` // } // // This means that if the producer produces []T, // the consumer must consume [][]T. // // There are cases where it's desirable // for the producer (the fx.Out) to produce multiple values ([]T), // and for the consumer (the fx.In) consume them as a single slice ([]T). // Fx offers flattened value groups for this purpose. // // To provide multiple values for a group from a result struct, produce a // slice and use the `,flatten` option on the group tag. This indicates that // each element in the slice should be injected into the group individually. // // type HandlerResult struct { // fx.Out // // Handler []Handler `group:"server,flatten"` // // Consumed as []Handler in ServerParams. // } // // # Unexported fields // // By default, a type that embeds fx.In may not have any unexported fields. The // following will return an error if used with Fx. // // type Params struct { // fx.In // // Logger *zap.Logger // mu sync.Mutex // } // // If you have need of unexported fields on such a type, you may opt-into // ignoring unexported fields by adding the ignore-unexported struct tag to the // fx.In. For example, // // type Params struct { // fx.In `ignore-unexported:"true"` // // Logger *zap.Logger // mu sync.Mutex // } package fx // import "go.uber.org/fx" ================================================ FILE: docs/.gitattributes ================================================ # Mark the uv.lock as generated so it is collapsed in review by default. uv.lock linguist-generated ================================================ FILE: docs/.gitignore ================================================ /_site ================================================ FILE: docs/Makefile ================================================ .PHONY: build build: uv run mkdocs build .PHONY: serve serve: uv run mkdocs serve ================================================ FILE: docs/ex/annotate/cast.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package annotate import ( "net/http" "go.uber.org/fx" "go.uber.org/fx/docs/ex/annotate/github" ) // HTTPClient matches the http.Client interface. // --8<-- [start:interface] type HTTPClient interface { Do(*http.Request) (*http.Response, error) } // This is a compile-time check that verifies // that our interface matches the API of http.Client. var _ HTTPClient = (*http.Client)(nil) // --8<-- [end:interface] // Config specifies the configuration of a client. type Config struct{} // NewHTTPClient builds a new HTTP client. // --8<-- [start:constructor] func NewHTTPClient(Config) (*http.Client, error) { // --8<-- [end:constructor] return http.DefaultClient, nil } // NewGitHubClient builds a new GitHub client. // --8<-- [start:iface-consumer] func NewGitHubClient(client HTTPClient) *github.Client { // --8<-- [end:iface-consumer] return new(github.Client) } func options() fx.Option { return fx.Options( // --8<-- [start:provides] fx.Provide( fx.Annotate( NewHTTPClient, fx.As(new(HTTPClient)), ), NewGitHubClient, ), // --8<-- [end:provides] ) } ================================================ FILE: docs/ex/annotate/cast_bad.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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:build ignore // +build ignore package annotate import ( "net/http" "go.uber.org/fx" "go.uber.org/fx/docs/ex/annotate/github" ) // NewGitHubClient builds a new GitHub client. // --8<-- [start:struct-consumer] func NewGitHubClient(client *http.Client) *github.Client { // --8<-- [end:struct-consumer] return new(github.Client) } func options() fx.Option { return fx.Options( // --8<-- [start:provides] fx.Provide( NewHTTPClient, NewGitHubClient, ), // --8<-- [end:provides] ) } ================================================ FILE: docs/ex/annotate/cast_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package annotate import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/docs/ex/annotate/github" "go.uber.org/fx/fxtest" ) func TestCast(t *testing.T) { var gh *github.Client app := fxtest.New(t, fx.Supply(Config{}), options(), fx.Populate(&gh), ) app.RequireStart().RequireStop() assert.NotNil(t, gh) } ================================================ FILE: docs/ex/annotate/github/stub.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package github // Client is a stub of a GitHub client. type Client struct{} ================================================ FILE: docs/ex/annotate/sample.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package annotate import ( "go.uber.org/fx" ) func howToAnnotate() (before, after fx.Option) { before = fx.Options( // --8<-- [start:before] fx.Provide( NewHTTPClient, ), // --8<-- [end:before] ) after = fx.Options( // --8<-- [start:wrap-1] // --8<-- [start:annotate] fx.Provide( fx.Annotate( NewHTTPClient, // --8<-- [end:wrap-1] fx.ResultTags(`name:"client"`), // --8<-- [start:wrap-2] ), ), // --8<-- [end:annotate] // --8<-- [end:wrap-2] ) return before, after } ================================================ FILE: docs/ex/annotate/sample_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package annotate import ( "net/http" "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestHowToAnnotate(t *testing.T) { before, after := howToAnnotate() t.Run("before", func(t *testing.T) { var got *http.Client app := fxtest.New(t, fx.Supply(Config{}), before, fx.Populate(&got), ) app.RequireStart().RequireStop() assert.NotNil(t, got) }) t.Run("after", func(t *testing.T) { var got struct { fx.In Client *http.Client `name:"client"` } app := fxtest.New(t, fx.Supply(Config{}), after, fx.Populate(&got), ) app.RequireStart().RequireStop() assert.NotNil(t, got.Client) }) } ================================================ FILE: docs/ex/get-started/01-minimal/main.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. // --8<-- [start:main] package main import "go.uber.org/fx" func main() { fx.New().Run() } // --8<-- [end:main] ================================================ FILE: docs/ex/get-started/01-minimal/main_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "testing" "go.uber.org/fx/docs/internal/apptest" ) func TestRun(t *testing.T) { apptest.Start(t, main) } ================================================ FILE: docs/ex/get-started/02-http-server/main.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "context" "fmt" "net" "net/http" "go.uber.org/fx" ) // --8<-- [start:provide-server-1] func main() { // --8<-- [start:app] fx.New( fx.Provide(NewHTTPServer), // --8<-- [end:provide-server-1] fx.Invoke(func(*http.Server) {}), // --8<-- [start:provide-server-2] ).Run() // --8<-- [end:app] } // --8<-- [end:provide-server-2] // --8<-- [start:partial-1] // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. // --8<-- [start:full] func NewHTTPServer(lc fx.Lifecycle) *http.Server { srv := &http.Server{Addr: ":8080"} // --8<-- [end:partial-1] lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } fmt.Println("Starting HTTP server at", srv.Addr) go srv.Serve(ln) return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) // --8<-- [start:partial-2] return srv } // --8<-- [end:partial-2] // --8<-- [end:full] ================================================ FILE: docs/ex/get-started/02-http-server/main_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/docs/internal/apptest" ) func TestRun(t *testing.T) { apptest.Start(t, main) res, err := http.Get("http://127.0.0.1:8080/hello") require.NoError(t, err) defer res.Body.Close() assert.Equal(t, http.StatusNotFound, res.StatusCode) } ================================================ FILE: docs/ex/get-started/03-echo-handler/main.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "context" "fmt" "io" "net" "net/http" "os" "go.uber.org/fx" ) func main() { fx.New( // --8<-- [start:provides] // --8<-- [start:provide-handler-1] fx.Provide( NewHTTPServer, // --8<-- [end:provide-handler-1] NewServeMux, // --8<-- [start:provide-handler-2] NewEchoHandler, ), // --8<-- [end:provides] fx.Invoke(func(*http.Server) {}), // --8<-- [end:provide-handler-2] ).Run() } // --8<-- [start:serve-mux] // NewServeMux builds a ServeMux that will route requests // to the given EchoHandler. func NewServeMux(echo *EchoHandler) *http.ServeMux { mux := http.NewServeMux() mux.Handle("/echo", echo) return mux } // --8<-- [end:serve-mux] // --8<-- [start:echo-handler] // EchoHandler is an http.Handler that copies its request body // back to the response. type EchoHandler struct{} // NewEchoHandler builds a new EchoHandler. func NewEchoHandler() *EchoHandler { return &EchoHandler{} } // ServeHTTP handles an HTTP request to the /echo endpoint. func (*EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { fmt.Fprintln(os.Stderr, "Failed to handle request:", err) } } // --8<-- [end:echo-handler] // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. // --8<-- [start:connect-mux] func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux) *http.Server { srv := &http.Server{Addr: ":8080", Handler: mux} lc.Append(fx.Hook{ // --8<-- [end:connect-mux] OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } fmt.Println("Starting HTTP server at", srv.Addr) go srv.Serve(ln) return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv } ================================================ FILE: docs/ex/get-started/03-echo-handler/main_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/apptest" "go.uber.org/fx/docs/internal/httptest" ) func TestRun(t *testing.T) { apptest.Start(t, main) got := httptest.PostSuccess(t, "http://127.0.0.1:8080/echo", "great success") assert.Equal(t, "great success", got) } ================================================ FILE: docs/ex/get-started/04-logger/main.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "context" "io" "net" "net/http" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/zap" ) // --8<-- [start:fx-logger] func main() { fx.New( fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), // --8<-- [end:fx-logger] // --8<-- [start:provides] fx.Provide( NewHTTPServer, NewServeMux, NewEchoHandler, zap.NewExample, ), // --8<-- [end:provides] fx.Invoke(func(*http.Server) {}), ).Run() } // NewServeMux builds a ServeMux that will route requests // to the given EchoHandler. func NewServeMux(echo *EchoHandler) *http.ServeMux { mux := http.NewServeMux() mux.Handle("/echo", echo) return mux } // EchoHandler is an http.Handler that copies its request body // back to the response. // --8<-- [start:echo-init-1] type EchoHandler struct { log *zap.Logger } // --8<-- [end:echo-init-1] // NewEchoHandler builds a new EchoHandler. // --8<-- [start:echo-init-2] func NewEchoHandler(log *zap.Logger) *EchoHandler { return &EchoHandler{log: log} } // --8<-- [end:echo-init-2] // ServeHTTP handles an HTTP request to the /echo endpoint. // --8<-- [start:echo-serve] func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { h.log.Warn("Failed to handle request", zap.Error(err)) } } // --8<-- [end:echo-serve] // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. // --8<-- [start:http-server] func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server { srv := &http.Server{Addr: ":8080", Handler: mux} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } log.Info("Starting HTTP server", zap.String("addr", srv.Addr)) go srv.Serve(ln) // --8<-- [end:http-server] return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv } ================================================ FILE: docs/ex/get-started/04-logger/main_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/apptest" "go.uber.org/fx/docs/internal/httptest" ) func TestRun(t *testing.T) { apptest.Start(t, main) got := httptest.PostSuccess(t, "http://127.0.0.1:8080/echo", "great success") assert.Equal(t, "great success", got) } ================================================ FILE: docs/ex/get-started/05-registration/main.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "context" "io" "net" "net/http" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/zap" ) func main() { fx.New( fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), // --8<-- [start:provides] fx.Provide( NewHTTPServer, NewServeMux, fx.Annotate( NewEchoHandler, fx.As(new(Route)), ), zap.NewExample, ), // --8<-- [end:provides] fx.Invoke(func(*http.Server) {}), ).Run() } // --8<-- [start:route] // Route is an http.Handler that knows the mux pattern // under which it will be registered. type Route interface { http.Handler // Pattern reports the path at which this is registered. Pattern() string } // --8<-- [end:route] // --8<-- [start:mux] // NewServeMux builds a ServeMux that will route requests // to the given Route. func NewServeMux(route Route) *http.ServeMux { mux := http.NewServeMux() mux.Handle(route.Pattern(), route) return mux } // --8<-- [end:mux] // EchoHandler is an http.Handler that copies its request body // back to the response. type EchoHandler struct { log *zap.Logger } // NewEchoHandler builds a new EchoHandler. func NewEchoHandler(log *zap.Logger) *EchoHandler { return &EchoHandler{log: log} } // Pattern reports the pattern under which // this handler should be registered. // --8<-- [start:echo-pattern] func (*EchoHandler) Pattern() string { return "/echo" } // --8<-- [end:echo-pattern] // ServeHTTP handles an HTTP request to the /echo endpoint. func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { h.log.Warn("Failed to handle request", zap.Error(err)) } } // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server { srv := &http.Server{Addr: ":8080", Handler: mux} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } log.Info("Starting HTTP server", zap.String("addr", srv.Addr)) go srv.Serve(ln) return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv } ================================================ FILE: docs/ex/get-started/05-registration/main_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/apptest" "go.uber.org/fx/docs/internal/httptest" ) func TestRun(t *testing.T) { apptest.Start(t, main) got := httptest.PostSuccess(t, "http://127.0.0.1:8080/echo", "great success") assert.Equal(t, "great success", got) } ================================================ FILE: docs/ex/get-started/06-another-handler/main.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "context" "fmt" "io" "net" "net/http" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/zap" ) func main() { fx.New( fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), // --8<-- [start:mux-provide] fx.Provide( NewHTTPServer, fx.Annotate( NewServeMux, fx.ParamTags(`name:"echo"`, `name:"hello"`), ), // --8<-- [end:mux-provide] // --8<-- [start:hello-provide-partial-1] // --8<-- [start:route-provides] fx.Annotate( NewEchoHandler, fx.As(new(Route)), // --8<-- [end:hello-provide-partial-1] fx.ResultTags(`name:"echo"`), // --8<-- [start:hello-provide-partial-2] ), fx.Annotate( NewHelloHandler, fx.As(new(Route)), // --8<-- [end:hello-provide-partial-2] fx.ResultTags(`name:"hello"`), // --8<-- [start:hello-provide-partial-3] ), // --8<-- [end:hello-provide-partial-3] // --8<-- [end:route-provides] zap.NewExample, ), fx.Invoke(func(*http.Server) {}), ).Run() } // Route is an http.Handler that knows the mux pattern // under which it will be registered. type Route interface { http.Handler // Pattern reports the path at which this is registered. Pattern() string } // --8<-- [start:mux] // NewServeMux builds a ServeMux that will route requests // to the given routes. func NewServeMux(route1, route2 Route) *http.ServeMux { mux := http.NewServeMux() mux.Handle(route1.Pattern(), route1) mux.Handle(route2.Pattern(), route2) return mux } // --8<-- [end:mux] // --8<-- [start:hello-init] // HelloHandler is an HTTP handler that // prints a greeting to the user. type HelloHandler struct { log *zap.Logger } // NewHelloHandler builds a new HelloHandler. func NewHelloHandler(log *zap.Logger) *HelloHandler { return &HelloHandler{log: log} } // --8<-- [end:hello-init] // Pattern reports the pattern under which // this handler should be registered. // --8<-- [start:hello-methods-1] func (*HelloHandler) Pattern() string { return "/hello" } // --8<-- [end:hello-methods-1] // ServeHTTP handles an HTTP request to the /hello endpoint. // --8<-- [start:hello-methods-2] func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { h.log.Error("Failed to read request", zap.Error(err)) http.Error(w, "Internal server error", http.StatusInternalServerError) return } if _, err := fmt.Fprintf(w, "Hello, %s\n", body); err != nil { h.log.Error("Failed to write response", zap.Error(err)) http.Error(w, "Internal server error", http.StatusInternalServerError) return } } // --8<-- [end:hello-methods-2] // EchoHandler is an http.Handler that copies its request body // back to the response. type EchoHandler struct { log *zap.Logger } // NewEchoHandler builds a new EchoHandler. func NewEchoHandler(log *zap.Logger) *EchoHandler { return &EchoHandler{log: log} } // Pattern reports the pattern under which // this handler should be registered. func (*EchoHandler) Pattern() string { return "/echo" } // ServeHTTP handles an HTTP request to the /echo endpoint. func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { h.log.Warn("Failed to handle request", zap.Error(err)) } } // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server { srv := &http.Server{Addr: ":8080", Handler: mux} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } log.Info("Starting HTTP server", zap.String("addr", srv.Addr)) go srv.Serve(ln) return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv } ================================================ FILE: docs/ex/get-started/06-another-handler/main_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/apptest" "go.uber.org/fx/docs/internal/httptest" ) func TestRun(t *testing.T) { apptest.Start(t, main) t.Run("echo", func(t *testing.T) { got := httptest.PostSuccess(t, "http://127.0.0.1:8080/echo", "great success") assert.Equal(t, "great success", got) }) t.Run("hello", func(t *testing.T) { got := httptest.PostSuccess(t, "http://127.0.0.1:8080/hello", "world") assert.Equal(t, "Hello, world\n", got) }) } ================================================ FILE: docs/ex/get-started/07-many-handlers/main.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "context" "fmt" "io" "net" "net/http" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/zap" ) func main() { fx.New( fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), // --8<-- [start:mux-provide] // --8<-- [start:route-provides-1] fx.Provide( // --8<-- [end:route-provides-1] NewHTTPServer, fx.Annotate( NewServeMux, fx.ParamTags(`group:"routes"`), ), // --8<-- [end:mux-provide] // --8<-- [start:route-provides-2] AsRoute(NewEchoHandler), AsRoute(NewHelloHandler), zap.NewExample, ), // --8<-- [end:route-provides-2] fx.Invoke(func(*http.Server) {}), ).Run() } // --8<-- [start:AsRoute] // AsRoute annotates the given constructor to state that // it provides a route to the "routes" group. func AsRoute(f any) any { return fx.Annotate( f, fx.As(new(Route)), fx.ResultTags(`group:"routes"`), ) } // --8<-- [end:AsRoute] // Route is an http.Handler that knows the mux pattern // under which it will be registered. type Route interface { http.Handler // Pattern reports the path at which this is registered. Pattern() string } // NewServeMux builds a ServeMux that will route requests // to the given routes. // --8<-- [start:mux] func NewServeMux(routes []Route) *http.ServeMux { mux := http.NewServeMux() for _, route := range routes { mux.Handle(route.Pattern(), route) } return mux } // --8<-- [end:mux] // HelloHandler is an HTTP handler that // prints a greeting to the user. type HelloHandler struct { log *zap.Logger } // NewHelloHandler builds a HelloHandler. func NewHelloHandler(log *zap.Logger) *HelloHandler { return &HelloHandler{log: log} } // Pattern reports the mux pattern under which // this handler should be registered. func (*HelloHandler) Pattern() string { return "/hello" } // ServeHTTP handles a request to the /hello endpoint. func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { h.log.Error("Failed to read request", zap.Error(err)) http.Error(w, "Internal server error", http.StatusInternalServerError) return } if _, err := fmt.Fprintf(w, "Hello, %s\n", body); err != nil { h.log.Error("Failed to write response", zap.Error(err)) http.Error(w, "Internal server error", http.StatusInternalServerError) return } } // EchoHandler is an http.Handler that copies its request body // back to the response. type EchoHandler struct { log *zap.Logger } // NewEchoHandler builds a new EchoHandler. func NewEchoHandler(log *zap.Logger) *EchoHandler { return &EchoHandler{log: log} } // Pattern reports the mux pattern under which // this handler should be registered. func (*EchoHandler) Pattern() string { return "/echo" } // ServeHTTP handles an HTTP request to the /echo endpoint. func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { h.log.Warn("Failed to handle request", zap.Error(err)) } } // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server { srv := &http.Server{Addr: ":8080", Handler: mux} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } log.Info("Starting HTTP server", zap.String("addr", srv.Addr)) go srv.Serve(ln) return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv } ================================================ FILE: docs/ex/get-started/07-many-handlers/main_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package main import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/apptest" "go.uber.org/fx/docs/internal/httptest" ) func TestRun(t *testing.T) { apptest.Start(t, main) t.Run("echo", func(t *testing.T) { got := httptest.PostSuccess(t, "http://127.0.0.1:8080/echo", "great success") assert.Equal(t, "great success", got) }) t.Run("hello", func(t *testing.T) { got := httptest.PostSuccess(t, "http://127.0.0.1:8080/hello", "world") assert.Equal(t, "Hello, world\n", got) }) } ================================================ FILE: docs/ex/modules/module.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package modules import ( "go.uber.org/fx" "go.uber.org/zap" ) // Module is an example of an Fx module's skeleton. // --8<-- [start:start] var Module = fx.Module("server", // --8<-- [end:start] // --8<-- [start:provide] fx.Provide( New, ), // --8<-- [end:provide] // --8<-- [start:privateProvide] fx.Provide( fx.Private, parseConfig, ), // --8<-- [end:privateProvide] // --8<-- [start:invoke] fx.Invoke(startServer), // --8<-- [end:invoke] // --8<-- [start:decorate] fx.Decorate(wrapLogger), // --8<-- [end:decorate] // --8<-- [start:endProvide] ) // --8<-- [end:endProvide] // Config is the configuration of the server. // --8<-- [start:config] type Config struct { Addr string `yaml:"addr"` } // --8<-- [end:config] func parseConfig() (Config, error) { return Config{}, nil } // Params defines the parameters of the module. // --8<-- [start:params] type Params struct { fx.In Log *zap.Logger Config Config } // --8<-- [end:params] // Result defines the results of the module. // --8<-- [start:result] type Result struct { fx.Out Server *Server } // --8<-- [end:result] // New builds a new server. // --8<-- [start:new] func New(p Params) (Result, error) { // --8<-- [end:new] return Result{ Server: &Server{}, }, nil } // Server is the server. type Server struct{} // Start starts the server. func (*Server) Start() error { return nil } func startServer(srv *Server) error { return srv.Start() } func wrapLogger(log *zap.Logger) *zap.Logger { return log.With(zap.String("component", "mymodule")) } ================================================ FILE: docs/ex/modules/module_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package modules import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" "go.uber.org/zap" ) func TestModule(t *testing.T) { var got *Server app := fxtest.New(t, Module, fx.Supply(zap.NewNop()), fx.Populate(&got), ) app.RequireStart().RequireStop() assert.NotNil(t, got) } ================================================ FILE: docs/ex/parameter-objects/define.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package paramobject import ( "net/http" "go.uber.org/fx" "go.uber.org/zap" ) // Client sends requests to a server. type Client struct { url string http *http.Client log *zap.Logger } // ClientConfig defines the configuration for the client. type ClientConfig struct { URL string } // ClientParams defines the parameters necessary to build a client. // --8<-- [start:empty-1] // --8<-- [start:fxin] // --8<-- [start:fields] type ClientParams struct { // --8<-- [end:empty-1] fx.In // --8<-- [end:fxin] Config ClientConfig HTTPClient *http.Client // --8<-- [start:empty-2] } // --8<-- [end:fields] // --8<-- [end:empty-2] // NewClient builds a new client. // --8<-- [start:takeparam] // --8<-- [start:consume] func NewClient(p ClientParams) (*Client, error) { // --8<-- [end:takeparam] return &Client{ url: p.Config.URL, http: p.HTTPClient, // ... }, nil // --8<-- [end:consume] } ================================================ FILE: docs/ex/parameter-objects/define_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package paramobject import ( "net/http" "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestClientParams(t *testing.T) { client := new(http.Client) var got *Client app := fxtest.New(t, fx.Supply( ClientConfig{URL: "http://example.com"}, client, ), fx.Provide(NewClient), fx.Populate(&got), ) app.RequireStart().RequireStop() assert.Equal(t, "http://example.com", got.url) // == instead of assert.Equal to match pointers. assert.True(t, client == got.http, "HTTP client did not match") } ================================================ FILE: docs/ex/parameter-objects/extend.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package paramobject import ( "net/http" "go.uber.org/fx" "go.uber.org/zap" ) // Params defines the parameters of new. // --8<-- [start:start-1] // --8<-- [start:full] type Params struct { fx.In Config ClientConfig HTTPClient *http.Client // --8<-- [end:start-1] Logger *zap.Logger `optional:"true"` // --8<-- [start:start-2] } // --8<-- [end:start-2] // --8<-- [end:full] // New builds a new Client. // --8<-- [start:start-3] // --8<-- [start:consume] func New(p Params) (*Client, error) { // --8<-- [end:start-3] log := p.Logger if log == nil { log = zap.NewNop() } // ... // --8<-- [end:consume] return &Client{log: log}, nil } ================================================ FILE: docs/ex/parameter-objects/extend_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package paramobject import ( "net/http" "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" "go.uber.org/zap" ) func TestExtendParams(t *testing.T) { t.Run("absent", func(t *testing.T) { var got *Client app := fxtest.New(t, fx.Supply( ClientConfig{}, new(http.Client), ), fx.Provide(New), fx.Populate(&got), ) app.RequireStart().RequireStop() assert.NotNil(t, got.log) }) t.Run("present", func(t *testing.T) { var got *Client log := zap.NewExample() app := fxtest.New(t, fx.Supply( ClientConfig{}, new(http.Client), log, ), fx.Provide(New), fx.Populate(&got), ) app.RequireStart().RequireStop() // Log must be what we provided. assert.True(t, got.log == log, "log did not match") }) } ================================================ FILE: docs/ex/result-objects/define.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package resultobject import "go.uber.org/fx" // Client is a client to make requests. type Client struct{} // ClientResult holds the result of NewClient. // --8<-- [start:empty-1] // --8<-- [start:fxout] // --8<-- [start:fields] type ClientResult struct { // --8<-- [end:empty-1] fx.Out // --8<-- [end:fxout] Client *Client // --8<-- [start:empty-2] } // --8<-- [end:empty-2] // --8<-- [end:fields] // NewClient builds a new Client. // --8<-- [start:returnresult] // --8<-- [start:produce] func NewClient() (ClientResult, error) { // --8<-- [end:returnresult] client := &Client{ // ... } return ClientResult{Client: client}, nil } // --8<-- [end:produce] ================================================ FILE: docs/ex/result-objects/define_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package resultobject import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestClientResult(t *testing.T) { var got *Client app := fxtest.New(t, fx.Provide(NewClient), fx.Populate(&got), ) app.RequireStart().RequireStop() assert.NotNil(t, got) } ================================================ FILE: docs/ex/result-objects/extend.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package resultobject import "go.uber.org/fx" // Inspector inspects client state. type Inspector struct{} // Result is the result of this module. // --8<-- [start:full] // --8<-- [start:start-1] type Result struct { fx.Out Client *Client // --8<-- [end:start-1] Inspector *Inspector // --8<-- [start:start-2] } // --8<-- [end:start-2] // --8<-- [end:full] // New builds a result. // --8<-- [start:start-3] func New() (Result, error) { client := &Client{ // ... } // --8<-- [start:produce] return Result{ Client: client, // --8<-- [end:start-3] Inspector: &Inspector{ // ... }, // --8<-- [start:start-4] }, nil // --8<-- [end:start-4] // --8<-- [end:produce] } ================================================ FILE: docs/ex/result-objects/extend_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package resultobject import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestExtendResult(t *testing.T) { var ( client *Client inspector *Inspector ) app := fxtest.New(t, fx.Provide(New), fx.Populate(&client, &inspector), ) app.RequireStart().RequireStop() assert.NotNil(t, client) assert.NotNil(t, inspector) } ================================================ FILE: docs/ex/value-groups/consume/annotate.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package consume import "go.uber.org/fx" // PlainModule is an unannotated NewEmitter. var PlainModule = fx.Options( // --8<-- [start:provide-init] fx.Provide( NewEmitter, ), // --8<-- [end:provide-init] ) // AnnotateModule is the module defined in this file. var AnnotateModule = fx.Options( // --8<-- [start:provide-wrap-1] fx.Provide( // --8<-- [start:provide-annotate] fx.Annotate( NewEmitter, // --8<-- [end:provide-wrap-1] fx.ParamTags(`group:"watchers"`), // --8<-- [start:provide-wrap-2] ), // --8<-- [end:provide-annotate] ), // --8<-- [end:provide-wrap-2] ) // Emitter emits events type Emitter struct{ ws []Watcher } // NewEmitter builds an emitter. // --8<-- [start:new-init] // --8<-- [start:new-consume] func NewEmitter(watchers []Watcher) (*Emitter, error) { // --8<-- [end:new-init] for _, w := range watchers { // ... // --8<-- [end:new-consume] _ = w // unused } return &Emitter{ws: watchers}, nil } // EmitterFromModule is a module that holds EmitterFrom. var EmitterFromModule = fx.Options( fx.Provide( // --8<-- [start:annotate-variadic] fx.Annotate( EmitterFrom, fx.ParamTags(`group:"watchers"`), ), // --8<-- [end:annotate-variadic] ), ) // EmitterFrom builds an Emitter from the list of watchers. // --8<-- [start:new-variadic] func EmitterFrom(watchers ...Watcher) (*Emitter, error) { // --8<-- [end:new-variadic] return &Emitter{ws: watchers}, nil } ================================================ FILE: docs/ex/value-groups/consume/consume_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package consume import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestConsume(t *testing.T) { tests := []struct { name string module fx.Option }{ {"Params", ParamsModule}, {"Annotated", AnnotateModule}, {"EmitterFrom", EmitterFromModule}, } addWatcher := fx.Annotate( func() struct{} { return struct{}{} }, fx.As(new(Watcher)), fx.ResultTags(`group:"watchers"`), ) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var e *Emitter app := fxtest.New(t, tt.module, fx.Provide( addWatcher, addWatcher, addWatcher, ), fx.Populate(&e), ) app.RequireStart().RequireStop() assert.Len(t, e.ws, 3) }) } } ================================================ FILE: docs/ex/value-groups/consume/param.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package consume import "go.uber.org/fx" // Watcher watches for events. type Watcher interface{} // ParamsModule is the module defined in this file. var ParamsModule = fx.Options( // --8<-- [start:provide] fx.Provide(New), // --8<-- [end:provide] ) // Params is a parameter object. // --8<-- [start:param-tagged] // --8<-- [start:param-init-1] type Params struct { fx.In // ... // --8<-- [end:param-init-1] Watchers []Watcher `group:"watchers"` // --8<-- [start:param-init-2] } // --8<-- [end:param-init-2] // --8<-- [end:param-tagged] // Result is a list of watchers. type Result struct { fx.Out Emitter *Emitter } // New consumes a value group. // --8<-- [start:new-init] // --8<-- [start:new-consume] func New(p Params) (Result, error) { // ... // --8<-- [end:new-init] for _, w := range p.Watchers { // ... // --8<-- [end:new-consume] _ = w // unused } return Result{ Emitter: &Emitter{ws: p.Watchers}, }, nil } ================================================ FILE: docs/ex/value-groups/feed/annotate.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package feed import "go.uber.org/fx" // AnnotateModule is the module defined in this file. var AnnotateModule = fx.Options( // --8<-- [start:provide-init] fx.Provide( NewWatcher, ), // --8<-- [end:provide-init] // --8<-- [start:provide-wrap-1] fx.Provide( // --8<-- [start:provide-annotate] fx.Annotate( NewWatcher, // --8<-- [end:provide-wrap-1] fx.ResultTags(`group:"watchers"`), // --8<-- [start:provide-wrap-2] ), // --8<-- [end:provide-annotate] ), // --8<-- [end:provide-wrap-2] ) // FileWatcher watches files. type FileWatcher struct{} // FileWatcherModule provides a FileWatcher as a Watcher. var FileWatcherModule = fx.Options( fx.Provide( // --8<-- [start:annotate-fw] fx.Annotate( NewFileWatcher, fx.As(new(Watcher)), fx.ResultTags(`group:"watchers"`), ), // --8<-- [end:annotate-fw] ), ) // NewFileWatcher builds a new file watcher. // --8<-- [start:new-fw-init] func NewFileWatcher( /* ... */ ) (*FileWatcher, error) { // --8<-- [end:new-fw-init] return &FileWatcher{ // ... }, nil } // NewWatcher builds a watcher. // --8<-- [start:new-init] func NewWatcher( /* ... */ ) (Watcher, error) { // ... // --8<-- [end:new-init] return &FileWatcher{ // ... }, nil } ================================================ FILE: docs/ex/value-groups/feed/feed_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package feed import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestWatcherModules(t *testing.T) { tests := []struct { name string module fx.Option }{ {"Results", ResultModule}, {"Annotated", AnnotateModule}, {"FileWatcher", FileWatcherModule}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var out struct { fx.In Watchers []Watcher `group:"watchers"` } app := fxtest.New(t, tt.module, fx.Populate(&out), ) app.RequireStart().RequireStop() assert.Len(t, out.Watchers, 1) }) } } ================================================ FILE: docs/ex/value-groups/feed/result.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package feed import "go.uber.org/fx" // ResultModule is the module defined in this file. var ResultModule = fx.Options( // --8<-- [start:provide] fx.Provide(New), // --8<-- [end:provide] ) // Watcher watches for events. type Watcher interface{} type watcher struct{} // Result is the result of an operation. // --8<-- [start:result-tagged] // --8<-- [start:result-init-1] type Result struct { fx.Out // ... // --8<-- [end:result-init-1] Watcher Watcher `group:"watchers"` // --8<-- [start:result-init-2] } // --8<-- [end:result-init-2] // --8<-- [end:result-tagged] // New produces a result object. // --8<-- [start:new-init-1] // --8<-- [start:new-watcher] func New( /* ... */ ) (Result, error) { // ... // --8<-- [end:new-init-1] watcher := &watcher{ // ... } // --8<-- [start:new-init-2] return Result{ // ... Watcher: watcher, }, nil } // --8<-- [end:new-watcher] // --8<-- [end:new-init-2] ================================================ FILE: docs/go.mod ================================================ module go.uber.org/fx/docs go 1.24 require ( github.com/stretchr/testify v1.8.1 go.uber.org/fx v1.18.2 go.uber.org/zap v1.26.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace go.uber.org/fx => ../ ================================================ FILE: docs/go.sum ================================================ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: docs/internal/apptest/run.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package apptest import ( "bufio" "context" "encoding/json" "os" "strings" "time" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/exectest" "go.uber.org/fx/docs/internal/test" ) // StartOption is an option for the Start function. type StartOption interface{ apply(*startOptions) } type startOptions struct { IsRunning func(string) bool Timeout time.Duration } type isRunningOption func(string) bool // IsRunning customizes how Start determines // whether a log statement represents a running application. // // Defaults to DefaultIsRunning. func IsRunning(f func(string) bool) StartOption { return isRunningOption(f) } func (o isRunningOption) apply(opts *startOptions) { opts.IsRunning = o } // DefaultIsRunning looks for lines in Fx's log output, // which match either the ConsoleLogger or the ZapLogger's // output, // and represent that the application is running // and ready to receive requests. func DefaultIsRunning(line string) bool { // ConsoleLogger if strings.Contains(line, "[Fx] RUNNING") { return true } // ZapLogger var log struct { Msg string `json:"msg"` } if err := json.Unmarshal([]byte(line), &log); err == nil { return log.Msg == "started" } return false } type timeoutOption time.Duration // Timeout specifies the duration after which we'll // stop waiting for the application to start up. // // Defaults to 5s. func Timeout(t time.Duration) StartOption { return timeoutOption(t) } func (o timeoutOption) apply(opts *startOptions) { opts.Timeout = time.Duration(o) } // Start starts the Fx application that main represents, // and blocks until isRunning(line) reports true for a line of output. // When the test exits, this signals for the application to stop, // and waits for it to stop. func Start(t test.T, main func(), options ...StartOption) { t.Helper() opts := startOptions{ Timeout: 5 * time.Second, IsRunning: DefaultIsRunning, } for _, o := range options { o.apply(&opts) } cmd := exectest.Command(t, main) r := exectest.StartWithOutput(t, cmd) done := make(chan struct{}) unblock := make(chan struct{}) go func() { defer close(done) var found bool scan := bufio.NewScanner(r) for scan.Scan() { line := scan.Text() t.Logf("%s", line) if !found && opts.IsRunning(line) { found = true close(unblock) } } assert.NoError(t, scan.Err(), "scan error") }() t.Cleanup(func() { assert.NoError(t, cmd.Process.Signal(os.Interrupt), "send SIGINT") <-done }) ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout) defer cancel() select { case <-unblock: return case <-done: // If the application exited without printing the Running // message, something went wrong. // Fail the test. t.Errorf("application exited unexpectedly") t.FailNow() case <-ctx.Done(): // Application did not start within the specified timeout. t.Errorf("application did not start in %v", opts.Timeout) t.FailNow() } } ================================================ FILE: docs/internal/apptest/run_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package apptest import ( "fmt" "os" "os/signal" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/docs/internal/test" ) func waitForInterrupt() { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) <-ch } func TestStart_FxLogger(t *testing.T) { Start(t, func() { fmt.Println("Not Fx output") fmt.Println("[Fx] RUNNING") waitForInterrupt() fmt.Println("Interrupted") }) // If we get here, everything is okay. } func TestStart_ZapLogger(t *testing.T) { Start(t, func() { fmt.Println("Not Fx output") fmt.Println(`{"msg": "started"}`) waitForInterrupt() fmt.Println("Interrupted") }) // If we get here, everything is okay. } func TestStart_Custom(t *testing.T) { Start(t, func() { fmt.Println("Not Fx output") fmt.Println("Hello world") waitForInterrupt() fmt.Println("Interrupted") }, IsRunning(func(s string) bool { return s == "Hello world" })) // If we get here, everything is okay. } func TestStart_UnexpectedExit(t *testing.T) { result := test.WithFake(t, func(t test.T) { Start(t, func() { fmt.Println("Not what we want") }) }) assert.True(t, result.Fatally, "expected FailNow") require.Len(t, result.Errors, 1, "expected an error message") assert.Contains(t, result.Errors[0], "application exited unexpectedly") } func TestStart_Timoeut(t *testing.T) { result := test.WithFake(t, func(t test.T) { Start(t, func() { fmt.Println("Not what we want") waitForInterrupt() }, Timeout(time.Millisecond)) }) assert.True(t, result.Fatally, "expected FailNow") require.Len(t, result.Errors, 1, "expected an error message") assert.Contains(t, result.Errors[0], "application did not start") } ================================================ FILE: docs/internal/exectest/cmd.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package exectest import ( "os" "os/exec" "path/filepath" "github.com/stretchr/testify/require" "go.uber.org/fx/docs/internal/test" ) // Command builds an exec.Cmd that will run the given function as an external // executable. // // This operates by re-running the test executable to run only the current // test, and hijacking that test execution to run the main function. // // cmd := Command(t, func() { fmt.Println("hello") }) // got, err := cmd.Output() // ... // fmt.Println(string(got) == "hello\n") // true func Command(t test.T, main func()) *exec.Cmd { t.Helper() // This messes up the hijacking sometimes. // Keep it simple -- only top level tests can do this. require.NotContains(t, t.Name(), "/", "exectest.Command cannot be used with subtests") if filepath.Base(os.Args[0]) == t.Name() { // We can't get coverage for this block // because if the condition is true, // we're inside the subprocess. main() os.Exit(0) } exe, err := os.Executable() require.NoError(t, err, "determine executable") cmd := exec.Command(exe, "-test.run", "^"+t.Name()+"$") // Args[0] is the value of os.Args[0] for the new executable. // os.Args[0] is allowed to be different from the command. cmd.Args[0] = t.Name() return cmd } ================================================ FILE: docs/internal/exectest/cmd_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package exectest import ( "bytes" "fmt" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCommandSuccess(t *testing.T) { cmd := Command(t, func() { fmt.Println("hello world") }) out, err := cmd.Output() require.NoError(t, err) assert.Equal(t, "hello world\n", string(out)) assert.True(t, cmd.ProcessState.Exited(), "must exit") assert.Zero(t, cmd.ProcessState.ExitCode(), "exit code") } func TestCommandNonZero(t *testing.T) { cmd := Command(t, func() { fmt.Fprintln(os.Stderr, "great sadness") os.Exit(1) }) var stderr bytes.Buffer cmd.Stderr = &stderr err := cmd.Run() require.Error(t, err, "command must fail") assert.Equal(t, "great sadness\n", stderr.String()) assert.True(t, cmd.ProcessState.Exited(), "must exit") assert.Equal(t, 1, cmd.ProcessState.ExitCode(), "exit code") } ================================================ FILE: docs/internal/exectest/output.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package exectest import ( "io" "os" "os/exec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/docs/internal/test" ) // StartWithOutput starts the given command, // and returns an io.Reader that reads from both, // its stdout and stderr. // // At test end, the system will ensure that // the command finished running. func StartWithOutput(t test.T, cmd *exec.Cmd) io.Reader { t.Helper() r, w, err := os.Pipe() require.NoError(t, err, "create pipe") cmd.Stdout = w cmd.Stderr = w require.NoError(t, cmd.Start(), "start command") // Close the output writer because this process won't write to it // anymore. Only the spawned process will. assert.NoError(t, w.Close(), "close output writer") t.Cleanup(func() { _, err := cmd.Process.Wait() assert.NoError(t, err, "wait for end") assert.NoError(t, r.Close(), "close output reader") }) return r } ================================================ FILE: docs/internal/exectest/output_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package exectest import ( "fmt" "io" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStartOutputReader_Stdout(t *testing.T) { cmd := Command(t, func() { fmt.Println("hello") fmt.Println("world") }) r := StartWithOutput(t, cmd) got, err := io.ReadAll(r) require.NoError(t, err) assert.Equal(t, "hello\nworld\n", string(got)) } func TestStartOutputReader_Stderr(t *testing.T) { cmd := Command(t, func() { fmt.Fprintln(os.Stderr, "great") fmt.Fprintln(os.Stderr, "sadness") }) r := StartWithOutput(t, cmd) got, err := io.ReadAll(r) require.NoError(t, err) assert.Equal(t, "great\nsadness\n", string(got)) } func TestStartOutputReader_Combined(t *testing.T) { cmd := Command(t, func() { fmt.Println("foo") fmt.Fprintln(os.Stderr, "bar") fmt.Println("baz") fmt.Fprintln(os.Stderr, "qux") }) r := StartWithOutput(t, cmd) got, err := io.ReadAll(r) require.NoError(t, err) assert.Equal(t, "foo\nbar\nbaz\nqux\n", string(got)) } ================================================ FILE: docs/internal/httptest/http.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package httptest import ( "net/http" "strings" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/docs/internal/iotest" "go.uber.org/fx/docs/internal/test" ) // PostSuccess makes an HTTP POST request to the given URL, // and returns the response body. // // PostSuccess uses a text/plain content type, // and expects a 200 status code. func PostSuccess(t test.T, url, body string) string { t.Helper() res, err := http.Post(url, "text/plain", strings.NewReader(body)) require.NoError(t, err, "http post %q", url) defer func() { assert.NoError(t, res.Body.Close(), "close response body") }() assert.Equal(t, http.StatusOK, res.StatusCode, "status code did not match") return iotest.ReadAll(t, res.Body) } ================================================ FILE: docs/internal/httptest/http_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package httptest import ( "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/test" ) func TestPostSuccess(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) { _, err := io.Copy(w, r.Body) assert.NoError(t, err, "copy request body") }) mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) }) srv := httptest.NewServer(mux) defer srv.Close() t.Run("success", func(t *testing.T) { got := PostSuccess(t, srv.URL+"/success", "hello") assert.Equal(t, "hello", got) }) t.Run("failure", func(t *testing.T) { result := test.WithFake(t, func(t test.T) { PostSuccess(t, srv.URL+"/failure", "hello") }) assert.True(t, result.Failed, "test should fail") assert.Len(t, result.Errors, 1) assert.Contains(t, result.Errors[0], "status code did not match") }) } ================================================ FILE: docs/internal/iotest/read.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package iotest import ( "io" "github.com/stretchr/testify/require" "go.uber.org/fx/docs/internal/test" ) // ReadAll is an alias for io.ReadAll that fails the current test // and halts execution if the read fails. func ReadAll(t test.T, r io.Reader) string { t.Helper() b, err := io.ReadAll(r) require.NoError(t, err, "read all output") return string(b) } ================================================ FILE: docs/internal/iotest/read_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package iotest import ( "errors" "strings" "testing" "testing/iotest" "github.com/stretchr/testify/assert" "go.uber.org/fx/docs/internal/test" ) func TestReadAll(t *testing.T) { t.Run("success", func(t *testing.T) { got := ReadAll(t, strings.NewReader("hello")) assert.Equal(t, "hello", got) }) t.Run("failure", func(t *testing.T) { newT := fakeT{T: t} ReadAll(&newT, iotest.ErrReader(errors.New("great sadness"))) assert.True(t, newT.failed, "test must have failed") }) } type fakeT struct { test.T failed bool } func (t *fakeT) Errorf(msg string, args ...any) { t.failed = true } func (t *fakeT) FailNow() { t.failed = true } ================================================ FILE: docs/internal/test/fake.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package test import ( "fmt" "runtime" "sync" ) // FakeReport is the result of an execution with a fake T. type FakeReport struct { Failed bool Fatally bool Errors []string } // WithFake runs the given function with a fake T. // Failures reported to the fake will not affect the current test. // When the given function finishes running, // WithFake produces a report of the test run. func WithFake(t T, f func(T)) FakeReport { fake := fakeT{T: t} // t.FailNow calls runtime.Goexit to kill the current goroutine. // To guard against that, we'll run the given function // in a separate goroutine and if the user calls t.FailNow, // we'll record it and continue execution here. done := make(chan struct{}) go func() { defer close(done) f(&fake) }() <-done return FakeReport{ Failed: fake.failed, Fatally: fake.fatal, Errors: fake.errors, } } // fakeT is a fake implementation of a T. type fakeT struct { T mu sync.RWMutex fatal bool failed bool errors []string } var _ T = (*fakeT)(nil) func (t *fakeT) Errorf(msg string, args ...any) { t.mu.Lock() defer t.mu.Unlock() t.failed = true t.errors = append(t.errors, fmt.Sprintf(msg, args...)) } func (t *fakeT) FailNow() { t.mu.Lock() defer t.mu.Unlock() t.failed = true t.fatal = true runtime.Goexit() } ================================================ FILE: docs/internal/test/fake_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package test import ( "testing" "github.com/stretchr/testify/assert" ) func TestWithFake_Success(t *testing.T) { report := WithFake(t, func(t T) { t.Logf("success") }) assert.False(t, report.Failed) assert.False(t, report.Fatally) assert.Empty(t, report.Errors) } func TestWithFake_Failure(t *testing.T) { report := WithFake(t, func(t T) { t.Errorf("great sadness") }) assert.True(t, report.Failed) assert.False(t, report.Fatally) assert.Equal(t, []string{"great sadness"}, report.Errors) } func TestWithFake_Fatal(t *testing.T) { report := WithFake(t, func(t T) { t.FailNow() }) assert.True(t, report.Failed) assert.True(t, report.Fatally) } ================================================ FILE: docs/internal/test/t.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package test // T is an interface defining a subset of the *testing.T type's API. type T interface { Cleanup(func()) Errorf(string, ...any) FailNow() Helper() Logf(string, ...any) Name() string } ================================================ FILE: docs/internal/test/t_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package test import "testing" var _ T = (*testing.T)(nil) ================================================ FILE: docs/mkdocs.yml ================================================ site_name: Fx site_url: https://uber-go.github.io/fx site_description: >- A dependency injection system for Go. repo_url: https://github.com/uber-go/fx repo_name: uber-go/fx edit_uri: edit/master/docs/src/ # .md files reside inside the src directory. docs_dir: src # The generated site will be placed in the _site directory. # This is the default for GitHub Pages' upload-artifact action. site_dir: _site extra: analytics: provider: google property: G-4YWLTPJ46M # Treat all warnings as errors. strict: true # By default, mkdocs will turn "foo/bar.md" into "foo/bar/index.html", # linking to it as "foo/bar/". # This does not match what we were using previously (foo/bar.html). # So we'll disable this behavior. use_directory_urls: false validation: # Warn about Markdown files not listed in the nav. omitted_files: warn # If a link is /foo/bar.md, # turn it into relative to the src/ directory. absolute_links: relative_to_docs # Warn about broken internal links to pages or anchors. unrecognized_links: warn anchors: warn theme: name: material # Support dark and light mode. palette: - scheme: default toggle: icon: material/toggle-switch name: Switch to dark mode - scheme: slate toggle: icon: material/toggle-switch-off-outline name: Switch to light mode features: - content.action.edit # show an 'edit this page' button - content.code.copy # show 'copy' button on code blocks - content.tooltips # render alt text as tooltips - header.autohide # hide header on scroll - navigation.footer # show next/prev page footer - navigation.indexes # allow foo/index.md to be home for foo/ - navigation.instant # use SPA-style navigation - navigation.instant.progress # show loading progress for instant nav - search.suggest # show search suggestions - toc.follow # highlight current section in TOC - toc.integrate # merge TOC into nav sidebar plugins: # Downloads third-party assets at build time and bundles them with the site. # This avoids calling out to third-party servers when the site is viewed. # We'll do this only if the build is for 'master' - privacy: enabled: !ENV [MASTER_BUILD, false] # Enable search - search # Show Created/Modified dates - git-revision-date-localized: enabled: !ENV [CI, false] enable_creation_date: true fallback_to_build_date: true # Redirect old links to new ones. - redirects: redirect_maps: intro.md: index.md value-groups.md: value-groups/index.md markdown_extensions: - admonition # admonitions (info/warning/error/etc.) - attr_list # custom HTML attributes for Markdown elements - def_list # definition lists - md_in_html # HTML blocks tagged with Markdown contents - pymdownx.details # collapsible blocks # snippets enables including code snippets from other files # with the "--8<--" syntax. # # It will search for snippets in the provided base paths. # We put code samples in the "ex/" directory, so that's one of the base paths. - pymdownx.snippets: base_path: [ex] # Syntax-highlighting of code fences (```), # plus custom fences for Mermaid diagrams. - pymdownx.superfences: # Mermaid diagram support. custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format # Tabbed content blocks, e.g. "Language A" vs "Language B". - pymdownx.tabbed: alternate_style: true # recommended slugify: !!python/object/apply:pymdownx.slugs.slugify kwds: case: lower # :foo-bar: emoji syntax. # # See https://squidfunk.github.io/mkdocs-material/reference/icons-emojis/ # to search through available emojis. # The emojis are rendered into inline svgs at build time. - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg # GitHub-style task lists. - pymdownx.tasklist: custom_checkbox: true # recommended # Generate a TOC for all pages with a permalink for all headers. - toc: permalink: true nav: - Home: index.md - Get started: - get-started/index.md - get-started/minimal.md - get-started/http-server.md - get-started/echo-handler.md - get-started/logger.md - get-started/registration.md - get-started/another-handler.md - get-started/many-handlers.md - get-started/conclusion.md - Concepts: - Container: container.md - Lifecycle: lifecycle.md - Modules: modules.md - Features: - Parameter Objects: parameter-objects.md - Result Objects: result-objects.md - Annotations: annotate.md - Value groups: - value-groups/index.md - value-groups/feed.md - value-groups/consume.md - FAQ: faq.md - Community: - Contributing: contributing.md - Release notes: changelog.md - API Reference: https://pkg.go.dev/go.uber.org/fx # Pages that are not listed in the nav must be listed here. # not_in_nav: | # get-started/*.md # Also watch ex/ for changes # as that's where we store snippets. watch: - ex ================================================ FILE: docs/pyproject.toml ================================================ [tool.uv] dev-dependencies = [ "mkdocs-material>=9.5.33", "mkdocs-git-revision-date-localized-plugin>=1.2.7", "mkdocs>=1.6.0", "mkdocs-redirects>=1.2.1", ] [tool.uv.workspace] members = [] ================================================ FILE: docs/src/annotate.md ================================================ # Annotations You can annotate functions and values with the `fx.Annotate` function before passing them to `fx.Provide`, `fx.Supply`, `fx.Invoke`, `fx.Decorate`, or `fx.Replace`. This allows you to re-use a plain Go function to do the following without manually wrapping the function to use [parameter](parameter-objects.md) or [result](result-objects.md) objects. - [feed values to a value group](value-groups/feed.md#with-annotated-functions) - [consume values from a value group](value-groups/consume.md#with-annotated-functions) ## Annotating a function **Prerequisites** A function that: - does not accept a [parameter object](parameter-objects.md), when annotating with `fx.ParamTags`. - does not return a [result object](result-objects.md) when annotating with `fx.ResultTags`. **Steps** 1. Given a function that you're passing to `fx.Provide`, `fx.Invoke`, or `fx.Decorate`, ```go --8<-- "annotate/sample.go:before" ``` 2. Wrap the function with `fx.Annotate`. ```go --8<-- "annotate/sample.go:wrap-1" --8<-- "annotate/sample.go:wrap-2" ``` 3. Inside `fx.Annotate`, pass in your annotations. ```go --8<-- "annotate/sample.go:annotate" ``` This annotation tags the result of the function with a name. **Related resources** - [fx.Annotation](https://pkg.go.dev/go.uber.org/fx#Annotation) holds a list of all supported annotations. ## Casting structs to interfaces You can use function annotations to cast a struct value returned by a function into an interface consumed by another function. **Prerequisites** 1. A function that produces a struct or pointer value. ```go --8<-- "annotate/cast.go:constructor" ``` 2. A function that consumes the result of the producer. ```go --8<-- "annotate/cast_bad.go:struct-consumer" ``` 3. Both functions are provided to the Fx application. ```go --8<-- "annotate/cast_bad.go:provides" ``` **Steps** 1. Declare an interface that matches the API of the produced `*http.Client`. ```go --8<-- "annotate/cast.go:interface" ``` 2. Change the consumer to accept the interface instead of the struct. ```go --8<-- "annotate/cast.go:iface-consumer" ``` 3. Finally, annotate the producer with `fx.As` to state that it produces an interface value. ```go --8<-- "annotate/cast.go:provides" ``` With this change, - the annotated function now only puts the interface into the container - the producer's API remains unchanged - the consumer is decoupled from the implementation and independently testable ================================================ FILE: docs/src/container.md ================================================ # Container Container is the abstraction responsible for holding all constructors and values. It’s the primary means by which an application interacts with Fx. You teach the container about the needs of your application, how to perform certain operations, and then you let it handle actually running your application. Fx does not provide direct access to the container. Instead, you specify operations to perform on the container by providing `fx.Option`s to the `fx.New` constructor. ```go package fx type App func New(opts ...Option) *App func (app *App) Run() type Option func Provide(constructors ...interface{}) Option func Invoke(funcs ...interface{}) Option ``` Check the [API Reference](https://pkg.go.dev/go.uber.org/fx#Option) for a complete list of options and their behaviors. ## Providing values You must provide values to the container before you can use them. Fx provides two ways to provide values to the container: - `fx.Provide` for values that have a constructor. ```go fx.Provide( func(cfg *Config) *Logger { /* ... */ }, ) ``` This says that Fx should use this function to construct a `*Logger`, and that a `*Config` is required to build one. - `fx.Supply` for pre-built non-interface values. ```go fx.Provide( fx.Supply(&Config{ Name: "my-app", }), ) ``` This says that Fx should use the provided `*Config` as-is. **Important**: `fx.Supply` is only for non-interface values. See *When to use fx.Supply* for more details. Values provided to the container are available to all other constructors. In the example above, the `*Config` would become available to the `*Logger` constructor, and the `*Logger` to any other constructors that need it. ### When to use fx.Supply Usually, `fx.Provide` is the right choice because more often than not, constructing an object requires its dependencies. `fx.Supply` is a convenience function for the rare cases where that isn't true: standalone values that don't depend on anything else. ```go fx.Provide(func() *Config { return &Config{Name: "my-app"} }) // is the same as fx.Supply(&Config{Name: "my-app"}) ``` However, even then, `fx.Supply` comes with a caveat: it can only be used for non-interface values. ??? question "Why can't I use fx.Supply for interface values?" This is a technical limitation imposed by the fact that `fx.Supply` has to rely on runtime reflection to determine the type of the value. Passing an interface value to `fx.Supply` is a lossy operation: it loses the original interface type, only giving us `interface{}`, at which point reflection will only reveal the concrete type of the value. For example, consider: ```go var svc RepositoryService = &repoService{ ... } ``` If you were to pass `svc` to `fx.Supply`, the container would only know that it's a `*repoService`, and it will not know that you intend to use it as a `RepositoryService`. ## Using values Providing values to the container only makes them available to the application. It doesn't do anything with them yet. Constructors passed to `fx.Provide` are not called until they are needed. For example, the following won't do anything: ```go fx.New( fx.Provide(newHTTPServer), // provides an *http.Server ).Run() ``` You next have to tell the container what is needed, and what to do with it. Fx provides [`fx.Invoke`](https://pkg.go.dev/go.uber.org/fx#Invoke) for this purpose. In the example above, we'll want an invocation that starts the server: ```go fx.New( fx.Provide(newHTTPServer), fx.Invoke(startHTTPServer), ).Run() ``` ### When to use fx.Invoke `fx.Invoke` is typically used for root-level invocations, like starting a server or running a main loop. It's also useful for invoking functions that have side effects. Examples of cases where you might use `fx.Invoke`: - Starting a background worker - Configuring a global logger As an example, consider an application organized into many distinct abstractions. ```mermaid flowchart LR CacheWarmer --> Redis Server[http.Server] --> UserHandler & PostHandler UserHandler --> Redis[redis.Client] & Client[http.Client] PostHandler --> sqlDB[sql.DB] subgraph Roots CacheWarmer Server end ``` `CacheWarmer` and `http.Server` are the roots of the application. We'll need `fx.Invoke` for the side effects of starting the server and the cache warmer loop. Everything else will be handled by the container automatically. ================================================ FILE: docs/src/faq.md ================================================ # Frequently Asked Questions This page contains answers to common questions and issues with using Fx. ## Does the order of `fx.Option`s matter? No, the order in which you provide Fx options to `fx.Options`, `fx.New`, `fx.Module`, and others does not matter. Ordering of options relative to each other is as follows: * Adding values: Operations like `fx.Provide` and `fx.Supply` are run in dependency order. Dependencies are determined by the function parameters and results. ```go // The following are all equivalent: fx.Options(fx.Provide(ParseConfig, NewLogger)) fx.Options(fx.Provide(NewLogger, ParseConfig)) fx.Options(fx.Provide(ParseConfig), fx.Provide(NewLogger)) fx.Options(fx.Provide(NewLogger), fx.Provide(ParseConfig)) ``` * Consuming values: Operations like `fx.Invoke` and `fx.Populate` are run after their dependencies have been satisfied: after `fx.Provide`s. Relative to each other, invokes are run in the order they were specified. ```go fx.Invoke(a, b) // a() is run before b() ``` `fx.Module` hierarchies affect invocation order: invocations in a parent module are run after those of a child module. ```go fx.Options( fx.Invoke(a), fx.Module("child", fx.Invoke(b)), ), // b() is run before a() ``` * Replacing values: Operations like `fx.Decorate` and `fx.Replace` are run after the Provide operations that they depend on, but before the Invoke operations that consume those values. Ordering of decorations relative to each other is determined by `fx.Module` hierarchies: decorations in a parent module are applied after those of a child module. ## Why does `fx.Supply` not accept interfaces? This is a technical limitation of how reflection in Go works. Suppose you have: ```go var redisClient ClientInterface = &redis.Client{ ... } ``` When you call `fx.Supply(redisClient)`, the knowledge that you intended to use this as a `ClientInterface` is lost. Fx has to use runtime reflection to inspect the type of the value, and at that point the Go runtime only tells it that it’s a `*redis.Client`. You can work around this with the `fx.Annotate` function and the `fx.As` annotation. ```go fx.Supply( fx.Annotate(redisClient, fx.As(new(ClientInterface))), ) ``` ================================================ FILE: docs/src/get-started/another-handler.md ================================================ # Register another handler The handler we defined above has a single handler. Let's add another. 1. Build a new handler in the same file. ```go --8<-- "get-started/06-another-handler/main.go:hello-init" ``` 2. Implement the `Route` interface for this handler. ```go --8<-- "get-started/06-another-handler/main.go:hello-methods-1" --8<-- "get-started/06-another-handler/main.go:hello-methods-2" ``` The handler reads its request body, and writes a welcome message back to the caller. 3. Provide this to the application as a `Route` next to `NewEchoHandler`. ```go --8<-- "get-started/06-another-handler/main.go:hello-provide-partial-1" --8<-- "get-started/06-another-handler/main.go:hello-provide-partial-2" --8<-- "get-started/06-another-handler/main.go:hello-provide-partial-3" ``` 4. Run the application--the service will fail to start. ``` [Fx] PROVIDE *http.Server <= main.NewHTTPServer() [Fx] PROVIDE *http.ServeMux <= main.NewServeMux() [Fx] PROVIDE main.Route <= fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]]) [Fx] Error after options were applied: fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from: [...] [Fx] ERROR Failed to start: the following errors occurred: - fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from: [...] Failed: cannot provide function "main".NewHelloHandler ([..]/main.go:53): cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80) ``` That's a lot of output, but inside the error message, we see: ``` cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80) ``` This fails because Fx does not allow two instances of the same type to be present in the container without annotating them. `NewServeMux` does not know which `Route` to use. Let's fix this. 5. Annotate `NewEchoHandler` and `NewHelloHandler` in `main()` with names for both handlers. ```go --8<-- "get-started/06-another-handler/main.go:route-provides" ``` 6. Add another Route parameter to `NewServeMux`. ```go --8<-- "get-started/06-another-handler/main.go:mux" ``` 7. Annotate `NewServeMux` in `main()` to pick these two *names values*. ```go --8<-- "get-started/06-another-handler/main.go:mux-provide" ``` 8. Run the program. ``` {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"name:\\\"echo\\\"\" \"name:\\\"hello\\\"\"])","type":"*http.ServeMux"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"name:\\\"echo\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"echo\"]"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"name:\\\"hello\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"hello\"]"} {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} {"level":"info","msg":"invoking","function":"main.main.func2()"} {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} {"level":"info","msg":"Starting HTTP server","addr":":8080"} {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"56.334µs"} {"level":"info","msg":"started"} ``` 9. Send requests to it. ``` $ curl -X POST -d 'hello' http://localhost:8080/echo hello $ curl -X POST -d 'gopher' http://localhost:8080/hello Hello, gopher ``` **What did we just do?** We added a constructor that produces a value with the same type as an existing type. We annotated constructors with `fx.ResultTags` to produce *named values*, and the consumer with `fx.ParamTags` to consume these named values. ================================================ FILE: docs/src/get-started/conclusion.md ================================================ # Conclusion This marks the end of this tutorial. In this tutorial, we covered, - [x] how to start an Fx application from scratch - [x] how to inject new dependencies and modify existing ones - [x] how to use interfaces to decouple components - [x] how to use named values - [x] how to use [value groups](/value-groups/index.md) ================================================ FILE: docs/src/get-started/echo-handler.md ================================================ # Register a handler We built a server that can receive requests, but it doesn't yet know how to handle them. Let's fix that. 1. Define a basic HTTP handler that copies the incoming request body to the response. Add the following to the bottom of your file. ```go --8<-- "get-started/03-echo-handler/main.go:echo-handler" ``` Provide this to the application. ```go --8<-- "get-started/03-echo-handler/main.go:provide-handler-1" --8<-- "get-started/03-echo-handler/main.go:provide-handler-2" ``` 2. Next, write a function that builds an `*http.ServeMux`. The `*http.ServeMux` will route requests received by the server to different handlers. To begin with, it will route requests sent to `/echo` to `*EchoHandler`, so its constructor should accept `*EchoHandler` as an argument. ```go --8<-- "get-started/03-echo-handler/main.go:serve-mux" ``` Likewise, provide this to the application. ```go --8<-- "get-started/03-echo-handler/main.go:provides" ``` Note that `NewServeMux` was added above `NewEchoHandler`--the order in which constructors are given to `fx.Provide` does not matter. 3. Lastly, modify the `NewHTTPServer` function to connect the server to this `*ServeMux`. ```go --8<-- "get-started/03-echo-handler/main.go:connect-mux" ``` 4. Run the server. ``` [Fx] PROVIDE *http.Server <= main.NewHTTPServer() [Fx] PROVIDE *http.ServeMux <= main.NewServeMux() [Fx] PROVIDE *main.EchoHandler <= main.NewEchoHandler() [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] INVOKE main.main.func1() [Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer) Starting HTTP server at :8080 [Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 7.459µs [Fx] RUNNING ``` 5. Send a request to the server. ```shell $ curl -X POST -d 'hello' http://localhost:8080/echo hello ``` **What did we just do?** We added more components with `fx.Provide`. These components declared dependencies on each other by adding parameters to their constructors. Fx will resolve component dependencies by parameters and return values of the provided functions. ================================================ FILE: docs/src/get-started/http-server.md ================================================ # Add an HTTP server In the previous section, we wrote a minimal Fx application that doesn't do anything. Let's add an HTTP server to it. 1. Write a function to build your HTTP server. ```go --8<-- "get-started/02-http-server/main.go:partial-1" --8<-- "get-started/02-http-server/main.go:partial-2" ``` This isn't enough, though--we need to tell Fx how to start the HTTP server. That's what the additional `fx.Lifecycle` argument is for. 2. Add a *lifecycle hook* to the application with the `fx.Lifecycle` object. This tells Fx how to start and stop the HTTP server. ```go --8<-- "get-started/02-http-server/main.go:full" ``` 3. Provide this to your Fx application above with `fx.Provide`. ```go --8<-- "get-started/02-http-server/main.go:provide-server-1" --8<-- "get-started/02-http-server/main.go:provide-server-2" ``` 4. Run the application. ``` [Fx] PROVIDE *http.Server <= main.NewHTTPServer() [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] RUNNING ``` Huh? Did something go wrong? The first line in the output states that the server was provided, but it doesn't include our "Starting HTTP server" message. The server didn't run. 5. To fix that, add an `fx.Invoke` that requests the constructed server. ```go --8<-- "get-started/02-http-server/main.go:app" ``` 6. Run the application again. This time we should see "Starting HTTP server" in the output. ``` [Fx] PROVIDE *http.Server <= main.NewHTTPServer() [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] INVOKE main.main.func1() [Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer) Starting HTTP server at :8080 [Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 7.958µs [Fx] RUNNING ``` 7. Send a request to the running server. ```shell $ curl http://localhost:8080 404 page not found ``` The request is a 404 because the server doesn't know how to handle it yet. We'll fix that in the next section. 8. Stop the application. ``` ^C [Fx] INTERRUPT [Fx] HOOK OnStop main.NewHTTPServer.func2() executing (caller: main.NewHTTPServer) [Fx] HOOK OnStop main.NewHTTPServer.func2() called by main.NewHTTPServer ran successfully in 129.875µs ``` **What did we just do?** We used `fx.Provide` to add an HTTP server to the application. The server hooks into the Fx application lifecycle--it will start serving requests when we call `App.Run`, and it will stop running when the application receives a stop signal. We used `fx.Invoke` to request that the HTTP server is always instantiated, even if none of the other components in the application reference it directly. **Related Resources** * [Application lifecycle](/lifecycle.md) further explains what Fx lifecycles are, and how to use them. ================================================ FILE: docs/src/get-started/index.md ================================================ # Get started with Fx This introduces you to the basics of Fx. In this tutorial you will: - [start an empty application](minimal.md) - [add an HTTP server to it](http-server.md) - [register a handler with the server](echo-handler.md) - [add logging to your application](logger.md) - [refactor to loosen coupling to your handler](registration.md) - [add another handler to the server](another-handler.md) - [generalize your implementation](many-handlers.md) First, get set up for the rest of the tutorial. 1. Start a new empty project. ```bash mkdir fxdemo cd fxdemo go mod init example.com/fxdemo ``` 2. Install the latest version of Fx. ```bash go get go.uber.org/fx@latest ``` Now begin by [creating a minimal application](minimal.md). ================================================ FILE: docs/src/get-started/logger.md ================================================ # Add a logger Our application currently prints the "Starting HTTP server" message to standard out, and errors to standard error. Both, standard out and error are also a form of global state. We should print to a logger object. We'll use [Zap](https://pkg.go.dev/go.uber.org/zap) in this section of the tutorial but you should be able to use any logging system. 1. Provide a Zap logger to the application. In this tutorial, we'll use [`zap.NewExample`](https://pkg.go.dev/go.uber.org/zap#NewExample), but for real applications, you should use `zap.NewProduction` or build a more customized logger. ```go --8<-- "get-started/04-logger/main.go:provides" ``` 2. Add a field to hold the logger on `EchoHandler`, and in `NewEchoHandler` add a new logger argument to set this field. ```go --8<-- "get-started/04-logger/main.go:echo-init-1" --8<-- "get-started/04-logger/main.go:echo-init-2" ``` 3. In the `EchoHandler.ServeHTTP` method, use the logger instead of printing to standard error. ```go --8<-- "get-started/04-logger/main.go:echo-serve" ``` 4. Similarly, update `NewHTTPServer` to expect a logger and log the "Starting HTTP server" message to that. ```go --8<-- "get-started/04-logger/main.go:http-server" ``` 5. (**Optional**) You can use the same Zap logger for Fx's own logs as well. ```go --8<-- "get-started/04-logger/main.go:fx-logger" ``` This will replace the `[Fx]` messages with messages printed to the logger. 6. Run the application. ``` {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"} {"level":"info","msg":"provided","constructor":"main.NewEchoHandler()","type":"*main.EchoHandler"} {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} {"level":"info","msg":"invoking","function":"main.main.func2()"} {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} {"level":"info","msg":"Starting HTTP server","addr":":8080"} {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"6.292µs"} {"level":"info","msg":"started"} ``` 7. Post a request to it. ```shell $ curl -X POST -d 'hello' http://localhost:8080/echo hello ``` **What did we just do?** We added another component to the application with `fx.Provide`, and injected that into other components that need to print messages. To do that, we only had to add a new parameter to the constructors. In the optional step, we told Fx that we'd like to provide a custom logger for Fx's own operations. We used the existing `fxevent.ZapLogger` to build this custom logger from our injected logger, so that all logs follow the same format. ================================================ FILE: docs/src/get-started/many-handlers.md ================================================ # Register many handlers We added two handlers in the previous section, but we reference them both explicitly by name when we build `NewServeMux`. This will quickly become inconvenient if we add more handlers. It's preferable if `NewServeMux` doesn't know how many handlers or their names, and instead just accepts a list of handlers to register. Let's do that. 1. Modify `NewServeMux` to operate on a list of `Route` objects. ```go --8<-- "get-started/07-many-handlers/main.go:mux" ``` 2. Annotate the `NewServeMux` entry in `main` to say that it accepts a slice that contains the contents of the "routes" group. ```go --8<-- "get-started/07-many-handlers/main.go:mux-provide" ``` 3. Define a new function `AsRoute` to build functions that feed into this group. ```go --8<-- "get-started/07-many-handlers/main.go:AsRoute" ``` 4. Wrap the `NewEchoHandler` and `NewHelloHandler` constructors in `main()` with `AsRoute` so that they feed their routes into this group. ```go --8<-- "get-started/07-many-handlers/main.go:route-provides-1" --8<-- "get-started/07-many-handlers/main.go:route-provides-2" ``` 5. Finally, run the application. ``` {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"group:\\\"routes\\\"\"])","type":"*http.ServeMux"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"} {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} {"level":"info","msg":"invoking","function":"main.main.func2()"} {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} {"level":"info","msg":"Starting HTTP server","addr":":8080"} {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"5µs"} {"level":"info","msg":"started"} ``` 6. Send requests to it. ``` $ curl -X POST -d 'hello' http://localhost:8080/echo hello $ curl -X POST -d 'gopher' http://localhost:8080/hello Hello, gopher ``` **What did we just do?** We annotated `NewServeMux` to consume a *value group* as a slice, and we annotated our existing handler constructors to feed into this value group. Any other constructor in the application can also feed values into this value group as long as the result conforms to the `Route` interface. They will all be collected together and passed into our `ServeMux` constructor. **Related Resources** * [Value groups](/value-groups/index.md) further explains what value groups are, and how to use them. ================================================ FILE: docs/src/get-started/minimal.md ================================================ # Create a minimal application Let's build the hello-world equivalent of an Fx application. This application won't do anything yet except print a bunch of logs. 1. Write a minimal `main.go`. ```go --8<-- "get-started/01-minimal/main.go:main" ``` 2. Run the application. ```bash go run . ``` You'll see output similar to the following. ``` [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] RUNNING ``` This shows the default objects provided to the Fx application, but it doesn't do anything meaningful yet. Stop the application with `Ctrl-C`. ``` [Fx] RUNNING ^C [Fx] INTERRUPT ``` **What did we just do?** We built an empty Fx application by calling `fx.New` with no arguments. Applications will normally pass arguments to `fx.New` to set up their components. We then run this application with the `App.Run` method. This method blocks until it receives a signal to stop, and it then runs any cleanup operations necessary before exiting. Fx is primarily intended for long-running server applications; these applications typically receive a signal from the deployment system when it's time to shut down. ================================================ FILE: docs/src/get-started/registration.md ================================================ # Decouple registration `NewServeMux` above declares an explicit dependency on `EchoHandler`. This is an unnecessarily tight coupling. Does the `ServeMux` really need to know the *exact* handler implementation? If we want to write tests for `ServeMux`, we shouldn't have to construct an `EchoHandler`. Let's try to fix this. 1. Define a `Route` type in your main.go. This is an extension of `http.Handler` where the handler knows its registration path. ```go --8<-- "get-started/05-registration/main.go:route" ``` 2. Modify `EchoHandler` to implement this interface. ```go --8<-- "get-started/05-registration/main.go:echo-pattern" ``` 3. In `main()`, annotate the `NewEchoHandler` entry to state that the handler should be provided as a Route. ```go --8<-- "get-started/05-registration/main.go:provides" ``` 4. Modify `NewServeMux` to accept a Route and use its provided pattern. ```go --8<-- "get-started/05-registration/main.go:mux" ``` 5. Run the service. ``` {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]])","type":"main.Route"} {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} {"level":"info","msg":"invoking","function":"main.main.func2()"} {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} {"level":"info","msg":"Starting HTTP server","addr":":8080"} {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"10.125µs"} {"level":"info","msg":"started"} ``` 6. Send a request to it. ```shell $ curl -X POST -d 'hello' http://localhost:8080/echo hello ``` **What did we just do?** We introduced an interface to decouple the implementation from the consumer. We then [annotated](../annotate.md) a previously provided constructor with `fx.Annotate` and `fx.As` to [cast its result to that interface](../annotate.md#casting-structs-to-interfaces). This way, `NewEchoHandler` was able to continue returning an `*EchoHandler`. ================================================ FILE: docs/src/index.md ================================================ # Fx Fx is **a dependency injection system for Go**.
- **Eliminate globals** --- By using Fx-managed singletons, you can eliminate global state from your application. With Fx, you don't have to rely on `init()` functions for setup, instead relying on Fx to manage the lifecycle of your application. - **Reduce boilerplate** --- Fx reduces the amount of code copy-pasted across your services. It lets you define shared application setup in a single place, and then reuse it across all your services. - **Automatic plumbing** --- Fx automatically constructs your application's dependency graph. A component added to the application can be used by any other component without any additional configuration. [Learn more about the dependency container :material-arrow-right:](container.md) - **Code reuse** --- Fx lets teams within your organization build loosely-coupled and well-integrated shareable components referred to as modules. [Learn more about modules :material-arrow-right:](modules.md) - **Battle-tested** Fx is the backbone of nearly all Go services at Uber.
[Get started :material-arrow-right-bold:](get-started/index.md){ .md-button .md-button--primary } ================================================ FILE: docs/src/lifecycle.md ================================================ # Application lifecycle The lifecycle of an Fx application has two high-level phases: *initialization* and *execution*. Both of these, in turn are comprised of multiple steps. During **initialization**, Fx will, - register all constructors passed to `fx.Provide` - register all decorators passed to `fx.Decorate` - run all functions passed to `fx.Invoke`, calling constructors and decorators as needed During **execution**, Fx will, - run all startup hooks appended to the application by providers, decorators, and invoked functions - wait for a signal to stop running - run all shutdown hooks appended to the application ```mermaid flowchart LR subgraph "Initialization (fx.New)" Provide --> Decorate --> Invoke end subgraph "Execution (fx.App.Run)" Start --> Wait --> Stop end Invoke --> Start style Wait stroke-dasharray: 5 5 ``` ## Lifecycle hooks Lifecycle hooks provide the ability to schedule work to be executed by Fx, when the application starts up or shuts down. Fx provides two kinds of hooks: - *Startup hooks*, also referred to as `OnStart` hooks. These run in the order they were appended. - *Shutdown hooks*, also referred to as `OnStop` hooks. These run in the **reverse** of the order they were appended. Typically, components that provide a startup hook also provide a corresponding shutdown hook to release the resources they acquired at startup. Fx runs both kinds of hooks with a hard timeout enforcement. Therefore, hooks are expected to block only as long as they need to *schedule* work. In other words, - hooks **must not** block to run long-running tasks synchronously - hooks **should** schedule long-running tasks in background goroutines - shutdown hooks **should** stop the background work started by startup hooks ================================================ FILE: docs/src/modules.md ================================================ --- sidebarDepth: 2 --- # Modules An Fx module is a shareable Go library or package that provides self-contained functionality to an Fx application. ## Writing modules To write an Fx module: 1. Define a top-level `Module` variable built from an `fx.Module` call. Give your module a short, memorable name for logs. ```go --8<-- "modules/module.go:start" ``` 2. Add components of your module with `fx.Provide`. ```go --8<-- modules/module.go:start modules/module.go:provide --8<-- ``` 3. If your module has a function that must always run, add an `fx.Invoke` with it. ```go --8<-- modules/module.go:start modules/module.go:invoke --8<-- ``` 4. If your module needs to decorate its dependencies before consuming them, add an `fx.Decorate` call for it. ```go --8<-- modules/module.go:start modules/module.go:decorate --8<-- ``` 5. Lastly, if you want to keep a constructor's outputs contained to your module (and modules your module includes), you can add an `fx.Private` when providing. ```go --8<-- modules/module.go:start modules/module.go:privateProvide --8<-- ``` In this case, `parseConfig` is now private to the "server" module. No modules that contain "server" will be able to use the resulting `Config` type because it can only be seen by the "server" module. That's all there's to writing modules. The rest of this section covers standards and conventions we've established for writing Fx modules at Uber. ### Naming #### Packages Standalone Fx modules, i.e. those distributed as an independent library, or those that have an independent Go package in a library, should be named for the library they wrap or the functionality they provide, with an added "fx" suffix. | Bad | Good | |--------------------|-------------------| | `package mylib` | `package mylibfx` | | `package httputil` | `package httpfx` | Fx modules that are part of another Go package, or single-serving modules written for a specific application may omit this suffix. #### Parameter and result objects Parameter and result object types should be named after the function they're for, by adding a `Params` or `Result` suffix to the function's name. **Exception**: If the function name begins with `New`, strip the `New` prefix before adding the `Params` or `Result` suffix. | Function | Parameter object | Result object | |----------|------------------|---------------| | New | Params | Result | | Run | RunParams | RunResult | | NewFoo | FooParams | FooResult | ### Export boundary functions Export functions which are used by your module via `fx.Provide` or `fx.Invoke` if that functionality would not be otherwise accessible. ```go --8<-- "modules/module.go:start" --8<-- "modules/module.go:provide" --8<-- "modules/module.go:privateProvide" --8<-- "modules/module.go:endProvide" --8<-- "modules/module.go:config" --8<-- "modules/module.go:new" ``` In this example, we don't export `parseConfig`, because it's a trivial `yaml.Decode` that we don't need to expose, but we still export `Config` so users can decode it themselves. **Rationale**: It should be possible to use your Fx module without using Fx itself. A user should be able to call the constructor directly and get the same functionality that the module would have provided with Fx. This is necessary for break-glass situations and partial migrations. ??? example "Bad: No way to build the server without Fx" ```go var Module = fx.Module("server", fx.Provide(newServer), ) func newServer(...) (*Server, error) ``` ### Use parameter objects Functions exposed by a module should not accept dependencies directly as parameters. Instead, they should use a [parameter object](parameter-objects.md). ```go --8<-- "modules/module.go:params" --8<-- "modules/module.go:new" ``` **Rationale**: Modules will inevitably need to declare new dependencies. By using parameter objects, we can [add new optional dependencies](parameter-objects.md#adding-new-parameters) in a backwards-compatible manner without changing the function signature. ??? example "Bad: Cannot add new parameters without breaking" ```go func New(log *zap.Logger) (Result, error) ``` ### Use result objects Functions exposed by a module should not declare their results as regular return values. Instead, they should use a [result object](result-objects.md). ```go --8<-- "modules/module.go:result" --8<-- "modules/module.go:new" ``` **Rationale**: Modules will inevitably need to return new results. By using result objects, we can [produce new results](result-objects.md#adding-new-results) in a backwards-compatible manner without changing the function signature. ??? example "Bad: Cannot add new results without breaking" ```go func New(Params) (*Server, error) ``` ### Don't provide what you don't own Fx modules should provide only those types to the application that are within their purview. Modules should not provide values they happen to use to the application. Nor should modules bundle other modules wholesale. **Rationale**: This leaves consumers free to choose how and where your dependencies come from. They can use the method you recommend (e.g., "include zapfx.Module"), or build their own variant of that dependency. ??? example "Bad: Provides a dependency" ```go package httpfx type Result struct { fx.Out Client *http.Client Logger *zap.Logger // BAD } ``` ??? example "Bad: Bundles another module" ```go package httpfx var Module = fx.Module("http", fx.Provide(New), zapfx.Module, // BAD ) ``` **Exception**: Organization or team-level "kitchen sink" modules that exists solely to bundle other modules may ignore this rule. For example, at Uber we define an `uberfx.Module` that bundles several other independent modules. Everything in this module is required by *all* services. ### Keep independent modules thin Independent Fx modules--those with names ending with "fx" rarely contain non-trivial business logic. If an Fx module is inside a package that contains significant business logic, it should not have the "fx" suffix in its name. **Rationale**: It should be possible for someone to migrate to or away from Fx, without rewriting their business logic. ??? example "Good: Business logic consumes net/http.Client" ```go package httpfx import "net/http" type Result struct { fx.Out Client *http.Client } ``` ??? example "Bad: Fx module implements logger" ```go package logfx type Logger struct { // ... } func New(...) Logger ``` ### Invoke sparingly Be deliberate in your choice to use `fx.Invoke` in your module. By design, Fx executes constructors added via `fx.Provide` only if the application consumes its result, either directly or indirectly, through another module, constructor, or invoke. On the other hand, functions added with `fx.Invoke` run unconditionally, and in doing so instantiate every direct and transitive value they depend on. ================================================ FILE: docs/src/parameter-objects.md ================================================ # Parameter Objects A parameter object is an object with the sole purpose of carrying parameters for a specific function or method. The object is typically defined exclusively for that function, and is not shared with other functions. That is, a parameter object is not a general purpose object like "user" but purpose-built, like "parameters for the `GetUser` function". In Fx, parameter objects contain exported fields exclusively, and are always tagged with `fx.In`. **Related** - [Result objects](result-objects.md) are the result analog of parameter objects. ## Using parameter objects To use parameter objects in Fx, take the following steps: 1. Define a new struct type named after your constructor with a `Params` suffix. If the constructor is named `NewClient`, name the struct `ClientParams`. If the constructor is named `New`, name the struct `Params`. This naming isn't strictly necessary, but it's a good convention to follow. ```go --8<-- "parameter-objects/define.go:empty-1" --8<-- "parameter-objects/define.go:empty-2" ``` 2. Embed `fx.In` into this struct. ```go --8<-- "parameter-objects/define.go:fxin" ``` 3. Add this new type as a parameter to your constructor *by value*. ```go --8<-- "parameter-objects/define.go:takeparam" ``` 4. Add dependencies of your constructor as **exported** fields on this struct. ```go --8<-- "parameter-objects/define.go:fields" ``` 5. Consume these fields in your constructor. ```go --8<-- "parameter-objects/define.go:consume" ``` Once you have a parameter object on a function, you can use it to access other advanced features of Fx: - [Consuming value groups with parameter objects](value-groups/consume.md#with-parameter-objects) ## Adding new parameters You can add new parameters for a constructor by adding new fields to a parameter object. For this to be backwards compatible, the new fields must be **optional**. 1. Take an existing parameter object. ```go --8<-- "parameter-objects/extend.go:start-1" --8<-- "parameter-objects/extend.go:start-2" --8<-- "parameter-objects/extend.go:start-3" ``` 2. Add a new field to it for your new dependency and **mark it optional** to keep this change backwards compatible. ```go --8<-- "parameter-objects/extend.go:full" ``` 3. In your constructor, consume this field. Be sure to handle the case when this field is absent -- it will take the zero value of its type in that case. ```go --8<-- "parameter-objects/extend.go:consume" ``` ================================================ FILE: docs/src/result-objects.md ================================================ # Result Objects A result object is an object with the sole purpose of carrying results for a specific function or method. As with [parameter objects](parameter-objects.md), the object is defined exclusively for a single function, and not shared with other functions. In Fx, result objects contain exported fields exclusively, and are always tagged with `fx.Out`. **Related** - [Parameter objects](parameter-objects.md) are the parameter analog of result objects. ## Using result objects To use result objects in Fx, take the following steps: 1. Define a new struct type named after your constructor with a `Result` suffix. If the constructor is named `NewClient`, name the struct `ClientResult`. If the constructor is named `New`, name the struct `Result`. This naming isn't strictly necessary, but it's a good convention to follow. ```go --8<-- "result-objects/define.go:empty-1" --8<-- "result-objects/define.go:empty-2" ``` 2. Embed `fx.Out` into this struct. ```go --8<-- "result-objects/define.go:fxout" ``` 3. Use this new type as the return value of your constructor *by value*. ```go --8<-- "result-objects/define.go:returnresult" ``` 4. Add values produced by your constructor as **exported** fields on this struct. ```go --8<-- "result-objects/define.go:fields" ``` 5. Set these fields and return an instance of this struct from your constructor. ```go --8<-- "result-objects/define.go:produce" ``` Once you have a result object on a function, you can use it to access other advanced features of Fx: - [Feeding value groups with result objects](value-groups/feed.md#with-result-objects) ## Adding new results You can add new values to an existing result object in a completely backwards compatible manner. 1. Take an existing result object. ```go --8<-- "result-objects/extend.go:start-1" --8<-- "result-objects/extend.go:start-2" --8<-- "result-objects/extend.go:start-3" --8<-- "result-objects/extend.go:start-4" ``` 2. Add a new field to it for your new result. ```go --8<-- "result-objects/extend.go:full" ``` 3. In your constructor, set this field. ```go --8<-- "result-objects/extend.go:produce" ``` ================================================ FILE: docs/src/value-groups/consume.md ================================================ # Consuming value groups To consume a value group of type `T`, you have to tag a `[]T` dependency with `group:"$name"` where `$name` is the name of the value group. You can do this in one of the following ways: - with parameter objects - with annotated functions ## With parameter objects You can use [parameter objects](../parameter-objects.md) to tag a slice parameter of a function as a value group. **Prerequisites** 1. A function that consumes a parameter object. ```go --8<-- "value-groups/consume/param.go:param-init-1" --8<-- "value-groups/consume/param.go:param-init-2" --8<-- "value-groups/consume/param.go:new-init" ``` 2. This function is provided to the Fx application. ```go --8<-- "value-groups/consume/param.go:provide" ``` **Steps** 1. Add a new **exported** field to the parameter object with the type `[]T`, where `T` is the kind of value in the value group. Tag this field with the name of the value group. ```go --8<-- "value-groups/consume/param.go:param-tagged" ``` 2. Consume this slice in the function that takes this parameter object. ```go --8<-- "value-groups/consume/param.go:new-consume" ``` !!! warning **Do not** rely on the order of values inside the slice. The order is randomized. ## With annotated functions You can use [annotations](../annotate.md) to consume a value group from an existing function. **Prerequisites** 1. A function that accepts a slice of the kind of values in the group. ```go --8<-- "value-groups/consume/annotate.go:new-init" ``` 2. The function is provided to the Fx application. ```go --8<-- "value-groups/consume/annotate.go:provide-init" ``` **Steps** 1. Wrap the function passed into `fx.Provide` with `fx.Annotate`. ```go --8<-- "value-groups/consume/annotate.go:provide-wrap-1" --8<-- "value-groups/consume/annotate.go:provide-wrap-2" ``` 2. Annotate this function to state that its slice parameter is a value group. ```go --8<-- "value-groups/consume/annotate.go:provide-annotate" ``` 3. Consume this slice in the function. ```go --8<-- "value-groups/consume/annotate.go:new-consume" ``` !!! tip "Functions can accept variadic arguments" You can consume a value group from a function that accepts variadic arguments instead of a slice. ```go --8<-- "value-groups/consume/annotate.go:new-variadic" ``` Annotate the variadic argument like it was a slice to do this. ```go --8<-- "value-groups/consume/annotate.go:annotate-variadic" ``` ================================================ FILE: docs/src/value-groups/feed.md ================================================ # Feeding value groups To feed values to a value group of type `T`, you have to tag a `T` result with `group:"$name"` where `$name` is the name of the value group. You can do this in one of the following ways: - with result objects - with annotated functions ## With result objects You can use [result objects](../result-objects.md) to tag the result of a function and feed it into a value group. **Prerequisites** 1. A function that produces a result object. ```go --8<-- value-groups/feed/result.go:result-init-1 value-groups/feed/result.go:result-init-2 value-groups/feed/result.go:new-init-1 value-groups/feed/result.go:new-init-2 --8<-- ``` 2. The function is provided to the Fx application. ```go --8<-- "value-groups/feed/result.go:provide" ``` **Steps** 1. Add a new **exported** field to the result object with the type of value you want to produce, and tag the field with the name of the value group. ```go --8<-- "value-groups/feed/result.go:result-tagged" ``` 2. In the function, set this new field to the value that you want to feed into the value group. ```go --8<-- "value-groups/feed/result.go:new-watcher" ``` ## With annotated functions You can use [annotations](../annotate.md) to send the result of an existing function to a value group. **Prerequisites** 1. A function that produces a value of the type required by the group. ```go --8<-- "value-groups/feed/annotate.go:new-init" ``` 2. The function is provided to the Fx application. ```go --8<-- "value-groups/feed/annotate.go:provide-init" ``` **Steps** 1. Wrap the function passed into `fx.Provide` with `fx.Annotate`. ```go --8<-- "value-groups/feed/annotate.go:provide-wrap-1" --8<-- "value-groups/feed/annotate.go:provide-wrap-2" ``` 2. Annotate this function to state that its result feeds into the value group. ```go --8<-- "value-groups/feed/annotate.go:provide-annotate" ``` !!! tip "Types don't always have to match" If the function you're annotating does not produce the same type as the group, but it can be cast into that type: ```go --8<-- "value-groups/feed/annotate.go:new-fw-init" ``` You can still use annotations to provide it to a value group. ```go --8<-- "value-groups/feed/annotate.go:annotate-fw" ``` See [casting structs to interfaces](../annotate.md#casting-structs-to-interfaces) for more details. ================================================ FILE: docs/src/value-groups/index.md ================================================ # Value Groups A *value group* is a collection of values of the same type. Any number of constructors across an Fx application can feed values into a value group. Similarly, any number of consumers can read from a value group without knowing about the full list of producers. ```mermaid flowchart TD group{{"[]Route"}} NewA & NewB & dots[...] & NewZ --> group group --> server[NewServeMux] group --> NewSiteMap style dots fill:none,stroke:none ``` !!! tip Fx produces the values fed into a value group in a random order. **Do not** make any assumptions about value group ordering. ## Using value groups To learn how to use value groups, see, - [Feeding value groups](feed.md) - [Consuming value groups](consume.md) ## Dependency strictness Dependencies formed by value groups can be: - strict: these are always consumed - soft: these are consumed only if the corresponding constructor was requested elsewhere By default, value group dependencies are strict. ### Strict value groups Strict value group dependencies are consumed by the value group regardless of whether their producers are otherwise used by the application. Suppose a constructor `NewFoo` produces two values: `A` and `B`. Value `A` feeds into the value group `[]A`, which is then consumed by function `Run`, and the application invokes function `Run` with `fx.Invoke`. With strict value groups, Fx will run `NewFoo` to populate the `[]A` group regardless of whether the application consumes the other result (`B`) directly or indirectly. ```mermaid flowchart LR subgraph NewFoo A; B end subgraph "fx.Invoke" Run end A --> group{{"[]A"}} --> Run ``` ### Soft value groups Soft value group dependencies are consumed by the value group only if the constructors that produce them were called by Fx anyway -- because the application consumes their other results directly or indirectly. Suppose we have a setup similar to the previous section, except that the value group is soft. ```mermaid flowchart LR subgraph NewFoo A; B end subgraph "fx.Invoke" Run end A -.-> group{{"[]A"}} --> Run ``` With soft value groups, Fx will run `NewFoo` to populate the `[]A` group only if `A` or `B` are consumed by another component in the application directly or indirectly. ```mermaid flowchart LR subgraph NewFoo A; B end subgraph "fx.Invoke" Run; Start end A -.-> group{{"[]A"}} --> Run B --> C --> Start ``` ================================================ FILE: error_example_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx_test import ( "errors" "fmt" "net/http" "os" "go.uber.org/fx" ) func ExampleError() { // A module that provides a HTTP server depends on // the $PORT environment variable. If the variable // is unset, the module returns an fx.Error option. newHTTPServer := func() fx.Option { port := os.Getenv("PORT") if port == "" { return fx.Error(errors.New("$PORT is not set")) } return fx.Provide(&http.Server{ Addr: fmt.Sprintf("127.0.0.1:%s", port), }) } app := fx.New( fx.NopLogger, newHTTPServer(), fx.Invoke(func(s *http.Server) error { return s.ListenAndServe() }), ) fmt.Println(app.Err()) // Output: // $PORT is not set } ================================================ FILE: example_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx_test import ( "context" "log" "net" "net/http" "os" "time" "go.uber.org/fx" "go.uber.org/fx/fxevent" ) // NewLogger constructs a logger. It's just a regular Go function, without any // special relationship to Fx. // // Since it returns a *log.Logger, Fx will treat NewLogger as the constructor // function for the standard library's logger. (We'll see how to integrate // NewLogger into an Fx application in the main function.) Since NewLogger // doesn't have any parameters, Fx will infer that loggers don't depend on any // other types - we can create them from thin air. // // Fx calls constructors lazily, so NewLogger will only be called only if some // other function needs a logger. Once instantiated, the logger is cached and // reused - within the application, it's effectively a singleton. // // By default, Fx applications only allow one constructor for each type. See // the documentation of the In and Out types for ways around this restriction. func NewLogger() *log.Logger { logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */) logger.Print("Executing NewLogger.") return logger } // NewHandler constructs a simple HTTP handler. Since it returns an // http.Handler, Fx will treat NewHandler as the constructor for the // http.Handler type. // // Like many Go functions, NewHandler also returns an error. If the error is // non-nil, Go convention tells the caller to assume that NewHandler failed // and the other returned values aren't safe to use. Fx understands this // idiom, and assumes that any function whose last return value is an error // follows this convention. // // Unlike NewLogger, NewHandler has formal parameters. Fx will interpret these // parameters as dependencies: in order to construct an HTTP handler, // NewHandler needs a logger. If the application has access to a *log.Logger // constructor (like NewLogger above), it will use that constructor or its // cached output and supply a logger to NewHandler. If the application doesn't // know how to construct a logger and needs an HTTP handler, it will fail to // start. // // Functions may also return multiple objects. For example, we could combine // NewHandler and NewLogger into a single function: // // func NewHandlerAndLogger() (*log.Logger, http.Handler, error) // // Fx also understands this idiom, and would treat NewHandlerAndLogger as the // constructor for both the *log.Logger and http.Handler types. Just like // constructors for a single type, NewHandlerAndLogger would be called at most // once, and both the handler and the logger would be cached and reused as // necessary. func NewHandler(logger *log.Logger) (http.Handler, error) { logger.Print("Executing NewHandler.") return http.HandlerFunc(func(http.ResponseWriter, *http.Request) { logger.Print("Got a request.") }), nil } // NewMux constructs an HTTP mux. Like NewHandler, it depends on *log.Logger. // However, it also depends on the Fx-specific Lifecycle interface. // // A Lifecycle is available in every Fx application. It lets objects hook into // the application's start and stop phases. In a non-Fx application, the main // function often includes blocks like this: // // srv, err := NewServer() // some long-running network server // if err != nil { // log.Fatalf("failed to construct server: %v", err) // } // // Construct other objects as necessary. // go srv.Start() // defer srv.Stop() // // In this example, the programmer explicitly constructs a bunch of objects, // crashing the program if any of the constructors encounter unrecoverable // errors. Once all the objects are constructed, we start any background // goroutines and defer cleanup functions. // // Fx removes the manual object construction with dependency injection. It // replaces the inline goroutine spawning and deferred cleanups with the // Lifecycle type. // // Here, NewMux makes an HTTP mux available to other functions. Since // constructors are called lazily, we know that NewMux won't be called unless // some other function wants to register a handler. This makes it easy to use // Fx's Lifecycle to start an HTTP server only if we have handlers registered. func NewMux(lc fx.Lifecycle, logger *log.Logger) *http.ServeMux { logger.Print("Executing NewMux.") // First, we construct the mux and server. We don't want to start the server // until all handlers are registered. mux := http.NewServeMux() server := &http.Server{ Addr: "127.0.0.1:8080", Handler: mux, } // If NewMux is called, we know that another function is using the mux. In // that case, we'll use the Lifecycle type to register a Hook that starts // and stops our HTTP server. // // Hooks are executed in dependency order. At startup, NewLogger's hooks run // before NewMux's. On shutdown, the order is reversed. // // Returning an error from OnStart hooks interrupts application startup. Fx // immediately runs the OnStop portions of any successfully-executed OnStart // hooks (so that types which started cleanly can also shut down cleanly), // then exits. // // Returning an error from OnStop hooks logs a warning, but Fx continues to // run the remaining hooks. lc.Append(fx.Hook{ // To mitigate the impact of deadlocks in application startup and // shutdown, Fx imposes a time limit on OnStart and OnStop hooks. By // default, hooks have a total of 15 seconds to complete. Timeouts are // passed via Go's usual context.Context. OnStart: func(context.Context) error { logger.Print("Starting HTTP server.") ln, err := net.Listen("tcp", server.Addr) if err != nil { return err } go server.Serve(ln) return nil }, OnStop: func(ctx context.Context) error { logger.Print("Stopping HTTP server.") return server.Shutdown(ctx) }, }) return mux } // Register mounts our HTTP handler on the mux. // // Register is a typical top-level application function: it takes a generic // type like ServeMux, which typically comes from a third-party library, and // introduces it to a type that contains our application logic. In this case, // that introduction consists of registering an HTTP handler. Other typical // examples include registering RPC procedures and starting queue consumers. // // Fx calls these functions invocations, and they're treated differently from // the constructor functions above. Their arguments are still supplied via // dependency injection and they may still return an error to indicate // failure, but any other return values are ignored. // // Unlike constructors, invocations are called eagerly. See the main function // below for details. func Register(mux *http.ServeMux, h http.Handler) { mux.Handle("/", h) } func Example() { app := fx.New( // Provide all the constructors we need, which teaches Fx how we'd like to // construct the *log.Logger, http.Handler, and *http.ServeMux types. // Remember that constructors are called lazily, so this block doesn't do // much on its own. fx.Provide( NewLogger, NewHandler, NewMux, ), // Since constructors are called lazily, we need some invocations to // kick-start our application. In this case, we'll use Register. Since it // depends on an http.Handler and *http.ServeMux, calling it requires Fx // to build those types using the constructors above. Since we call // NewMux, we also register Lifecycle hooks to start and stop an HTTP // server. fx.Invoke(Register), // This is optional. With this, you can control where Fx logs // its events. In this case, we're using a NopLogger to keep // our test silent. Normally, you'll want to use an // fxevent.ZapLogger or an fxevent.ConsoleLogger. fx.WithLogger( func() fxevent.Logger { return fxevent.NopLogger }, ), ) // In a typical application, we could just use app.Run() here. Since we // don't want this example to run forever, we'll use the more-explicit Start // and Stop. startCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := app.Start(startCtx); err != nil { log.Fatal(err) } // Normally, we'd block here with <-app.Done(). Instead, we'll make an HTTP // request to demonstrate that our server is running. if _, err := http.Get("http://localhost:8080/"); err != nil { log.Fatal(err) } stopCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := app.Stop(stopCtx); err != nil { log.Fatal(err) } // Output: // Executing NewLogger. // Executing NewMux. // Executing NewHandler. // Starting HTTP server. // Got a request. // Stopping HTTP server. } ================================================ FILE: extract.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "reflect" "unicode" "unicode/utf8" ) var _typeOfIn = reflect.TypeOf(In{}) // Extract fills the given struct with values from the dependency injection // container on application initialization. The target MUST be a pointer to a // struct. Only exported fields will be filled. // // Deprecated: Use Populate instead. func Extract(target any) Option { v := reflect.ValueOf(target) if t := v.Type(); t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { return Error(fmt.Errorf("Extract expected a pointer to a struct, got a %v", t)) } v = v.Elem() t := v.Type() // We generate a function which accepts a single fx.In struct as an // argument. This struct contains all exported fields of the target // struct. // Fields of the generated fx.In struct. fields := make([]reflect.StructField, 0, t.NumField()+1) // Anonymous dig.In field. fields = append(fields, reflect.StructField{ Name: _typeOfIn.Name(), Anonymous: true, Type: _typeOfIn, }) // List of values in the target struct aligned with the fields of the // generated struct. // // So for example, if the target is, // // var target struct { // Foo io.Reader // bar []byte // Baz io.Writer // } // // The generated struct has the shape, // // struct { // fx.In // // F0 io.Reader // F2 io.Writer // } // // And `targets` is, // // [ // target.Field(0), // Foo io.Reader // target.Field(2), // Baz io.Writer // ] // // As we iterate through the fields of the generated struct, we can copy // the value into the corresponding value in the targets list. targets := make([]reflect.Value, 0, t.NumField()) for i := 0; i < t.NumField(); i++ { f := t.Field(i) // Skip unexported fields. if f.Anonymous { // If embedded, StructField.PkgPath is not a reliable indicator of // whether the field is exported. See // https://github.com/golang/go/issues/21122 t := f.Type if t.Kind() == reflect.Ptr { t = t.Elem() } if !isExported(t.Name()) { continue } } else if f.PkgPath != "" { continue } // We don't copy over names or embedded semantics. fields = append(fields, reflect.StructField{ Name: fmt.Sprintf("F%d", i), Type: f.Type, Tag: f.Tag, }) targets = append(targets, v.Field(i)) } // Equivalent to, // // func(r struct { // fx.In // // F1 Foo // F2 Bar // }) { // target.Foo = r.F1 // target.Bar = r.F2 // } fn := reflect.MakeFunc( reflect.FuncOf( []reflect.Type{reflect.StructOf(fields)}, nil, /* results */ false, /* variadic */ ), func(args []reflect.Value) []reflect.Value { result := args[0] for i := 1; i < result.NumField(); i++ { targets[i-1].Set(result.Field(i)) } return nil }, ) return Invoke(fn.Interface()) } // isExported reports whether the identifier is exported. func isExported(id string) bool { r, _ := utf8.DecodeRuneInString(id) return unicode.IsUpper(r) } ================================================ FILE: extract_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx_test import ( "bytes" "fmt" "testing" . "go.uber.org/fx" "go.uber.org/fx/fxtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/dig" ) func TestExtract(t *testing.T) { t.Parallel() type type1 struct{} type type2 struct{} type type3 struct{} t.Run("Failures", func(t *testing.T) { t.Parallel() tests := []any{ 3, func() {}, struct{}{}, struct{ Foo *bytes.Buffer }{}, } for _, tt := range tests { t.Run(fmt.Sprintf("%T", tt), func(t *testing.T) { t.Parallel() app := NewForTest(t, Provide(func() *bytes.Buffer { return &bytes.Buffer{} }), Extract(&tt), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "Extract expected a pointer to a struct") }) } }) t.Run("ValidateApp", func(t *testing.T) { t.Parallel() tests := []any{ 3, func() {}, struct{}{}, struct{ Foo *bytes.Buffer }{}, } for _, tt := range tests { t.Run(fmt.Sprintf("%T", tt), func(t *testing.T) { t.Parallel() err := validateTestApp(t, Provide(func() *bytes.Buffer { return &bytes.Buffer{} }), Extract(&tt), ) require.Error(t, err) assert.Contains(t, err.Error(), "Extract expected a pointer to a struct") }) } }) t.Run("Empty", func(t *testing.T) { t.Parallel() new1 := func() *type1 { panic("new1 must not be called") } new2 := func() *type2 { panic("new2 must not be called") } var out struct{} app := fxtest.New(t, Provide(new1, new2), Extract(&out), ) app.RequireStart().RequireStop() }) t.Run("StructIsExtracted", func(t *testing.T) { t.Parallel() var gave1 *type1 new1 := func() *type1 { gave1 = &type1{} return gave1 } var gave2 *type2 new2 := func() *type2 { gave2 = &type2{} return gave2 } var out struct { T1 *type1 T2 *type2 } app := fxtest.New(t, Provide(new1, new2), Extract(&out), ) defer app.RequireStart().RequireStop() assert.NotNil(t, out.T1, "T1 must not be nil") assert.NotNil(t, out.T2, "T2 must not be nil") assert.True(t, gave1 == out.T1, "T1 must match") assert.True(t, gave2 == out.T2, "T2 must match") }) t.Run("EmbeddedExportedField", func(t *testing.T) { t.Parallel() type T1 struct{} var gave1 *T1 new1 := func() *T1 { gave1 = &T1{} return gave1 } var out struct{ *T1 } app := fxtest.New(t, Provide(new1), Extract(&out), ) defer app.RequireStart().RequireStop() assert.NotNil(t, out.T1, "T1 must not be nil") assert.True(t, gave1 == out.T1, "T1 must match") }) t.Run("EmbeddedUnexportedField", func(t *testing.T) { t.Parallel() new1 := func() *type1 { return &type1{} } var out struct{ *type1 } app := fxtest.New(t, Provide(new1), Extract(&out)) defer app.RequireStart().RequireStop() // Unexported fields are left unchanged. assert.Nil(t, out.type1, "type1 must be nil") }) t.Run("EmbeddedUnexportedFieldValue", func(t *testing.T) { t.Parallel() type type4 struct{ foo string } new4 := func() type4 { return type4{"foo"} } var out struct{ type4 } app := fxtest.New(t, Provide(new4), Extract(&out)) defer app.RequireStart().RequireStop() // Unexported fields are left unchanged. assert.NotEqual(t, "foo", out.foo) }) t.Run("DuplicateFields", func(t *testing.T) { t.Parallel() var gave *type1 new1 := func() *type1 { require.Nil(t, gave, "gave must be nil") gave = &type1{} return gave } var out struct { X *type1 Y *type1 } app := fxtest.New(t, Provide(new1), Extract(&out), ) defer app.RequireStart().RequireStop() assert.NotNil(t, out.X, "X must not be nil") assert.NotNil(t, out.Y, "Y must not be nil") assert.True(t, gave == out.X, "X must match") assert.True(t, gave == out.Y, "Y must match") }) t.Run("SkipsUnexported", func(t *testing.T) { t.Parallel() var gave1 *type1 new1 := func() *type1 { gave1 = &type1{} return gave1 } new2 := func() *type2 { panic("new2 must not be called") } var gave3 *type3 new3 := func() *type3 { gave3 = &type3{} return gave3 } var out struct { T1 *type1 t2 *type2 T3 *type3 } app := fxtest.New(t, Provide(new1, new2, new3), Extract(&out), ) defer app.RequireStart().RequireStop() assert.NotNil(t, out.T1, "T1 must not be nil") assert.Nil(t, out.t2, "t2 must be nil") assert.NotNil(t, out.T3, "T3 must not be nil") assert.True(t, gave1 == out.T1, "T1 must match") assert.True(t, gave3 == out.T3, "T3 must match") }) t.Run("DoesNotZeroUnexported", func(t *testing.T) { t.Parallel() var gave1 *type1 new1 := func() *type1 { gave1 = &type1{} return gave1 } new2 := func() *type2 { panic("new2 must not be called") } var out struct { T1 *type1 t2 *type2 } t2 := &type2{} out.t2 = t2 app := fxtest.New(t, Provide(new1, new2), Extract(&out), ) defer app.RequireStart().RequireStop() assert.NotNil(t, out.T1, "T1 must not be nil") assert.NotNil(t, out.t2, "t2 must not be nil") assert.True(t, gave1 == out.T1, "T1 must match") assert.True(t, t2 == out.t2, "t2 must match") }) t.Run("TopLevelDigIn", func(t *testing.T) { t.Parallel() var out struct{ dig.In } app := fxtest.New(t, Extract(&out)) defer app.RequireStart().RequireStop() }) t.Run("TopLevelFxIn", func(t *testing.T) { t.Parallel() new1 := func() *type1 { panic("new1 must not be called") } new2 := func() *type2 { panic("new2 must not be called") } var out struct{ In } app := fxtest.New(t, Provide(new1, new2), Extract(&out), ) defer app.RequireStart().RequireStop() }) t.Run("NestedFxIn", func(t *testing.T) { t.Parallel() var gave1 *type1 new1 := func() *type1 { gave1 = &type1{} return gave1 } var out struct { Result struct { In T1 *type1 T2 *type2 `optional:"true"` } } app := fxtest.New(t, Provide(new1), Extract(&out), ) defer app.RequireStart().RequireStop() assert.NotNil(t, out.Result.T1, "T1 must not be nil") assert.Nil(t, out.Result.T2, "T2 must be nil") assert.True(t, gave1 == out.Result.T1, "T1 must match") }) t.Run("FurtherNestedFxIn", func(t *testing.T) { t.Parallel() var out struct { In B struct { In C int } } app := fxtest.New(t, Provide(func() int { return 42 }), Extract(&out), ) defer app.RequireStart().RequireStop() assert.Equal(t, 42, out.B.C, "B.C must match") }) t.Run("FieldsCanBeOptional", func(t *testing.T) { t.Parallel() var gave1 *type1 new1 := func() *type1 { gave1 = &type1{} return gave1 } var out struct { T1 *type1 T2 *type2 `optional:"true"` } app := fxtest.New(t, Provide(new1), Extract(&out), ) defer app.RequireStart().RequireStop() assert.NotNil(t, out.T1, "T1 must not be nil") assert.Nil(t, out.T2, "T2 must be nil") assert.True(t, gave1 == out.T1, "T1 must match") }) } ================================================ FILE: fxevent/console.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxevent import ( "fmt" "io" "strings" ) // ConsoleLogger is an Fx event logger that attempts to write human-readable // messages to the console. // // Use this during development. type ConsoleLogger struct { W io.Writer } var _ Logger = (*ConsoleLogger)(nil) func (l *ConsoleLogger) logf(msg string, args ...any) { fmt.Fprintf(l.W, "[Fx] "+msg+"\n", args...) } // LogEvent logs the given event to the provided Zap logger. func (l *ConsoleLogger) LogEvent(event Event) { switch e := event.(type) { case *OnStartExecuting: l.logf("HOOK OnStart\t\t%s executing (caller: %s)", e.FunctionName, e.CallerName) case *OnStartExecuted: if e.Err != nil { l.logf("HOOK OnStart\t\t%s called by %s failed in %s: %+v", e.FunctionName, e.CallerName, e.Runtime, e.Err) } else { l.logf("HOOK OnStart\t\t%s called by %s ran successfully in %s", e.FunctionName, e.CallerName, e.Runtime) } case *OnStopExecuting: l.logf("HOOK OnStop\t\t%s executing (caller: %s)", e.FunctionName, e.CallerName) case *OnStopExecuted: if e.Err != nil { l.logf("HOOK OnStop\t\t%s called by %s failed in %s: %+v", e.FunctionName, e.CallerName, e.Runtime, e.Err) } else { l.logf("HOOK OnStop\t\t%s called by %s ran successfully in %s", e.FunctionName, e.CallerName, e.Runtime) } case *Supplied: if e.Err != nil { l.logf("ERROR\tFailed to supply %v: %+v", e.TypeName, e.Err) } else if e.ModuleName != "" { l.logf("SUPPLY\t%v from module %q", e.TypeName, e.ModuleName) } else { l.logf("SUPPLY\t%v", e.TypeName) } case *Provided: var privateStr string if e.Private { privateStr = " (PRIVATE)" } for _, rtype := range e.OutputTypeNames { if e.ModuleName != "" { l.logf("PROVIDE%v\t%v <= %v from module %q", privateStr, rtype, e.ConstructorName, e.ModuleName) } else { l.logf("PROVIDE%v\t%v <= %v", privateStr, rtype, e.ConstructorName) } } if e.Err != nil { l.logf("Error after options were applied: %+v", e.Err) } case *Replaced: for _, rtype := range e.OutputTypeNames { if e.ModuleName != "" { l.logf("REPLACE\t%v from module %q", rtype, e.ModuleName) } else { l.logf("REPLACE\t%v", rtype) } } if e.Err != nil { l.logf("ERROR\tFailed to replace: %+v", e.Err) } case *Decorated: for _, rtype := range e.OutputTypeNames { if e.ModuleName != "" { l.logf("DECORATE\t%v <= %v from module %q", rtype, e.DecoratorName, e.ModuleName) } else { l.logf("DECORATE\t%v <= %v", rtype, e.DecoratorName) } } if e.Err != nil { l.logf("Error after options were applied: %+v", e.Err) } case *BeforeRun: var moduleStr string if e.ModuleName != "" { moduleStr = fmt.Sprintf(" from module %q", e.ModuleName) } l.logf("BEFORE RUN\t%s: %s%s", e.Kind, e.Name, moduleStr) case *Run: var moduleStr string if e.ModuleName != "" { moduleStr = fmt.Sprintf(" from module %q", e.ModuleName) } l.logf("RUN\t%v: %v in %s%v", e.Kind, e.Name, e.Runtime, moduleStr) if e.Err != nil { l.logf("Error returned: %+v", e.Err) } case *Invoking: if e.ModuleName != "" { l.logf("INVOKE\t\t%s from module %q", e.FunctionName, e.ModuleName) } else { l.logf("INVOKE\t\t%s", e.FunctionName) } case *Invoked: if e.Err != nil { l.logf("ERROR\t\tfx.Invoke(%v) called from:\n%+vFailed: %+v", e.FunctionName, e.Trace, e.Err) } case *Stopping: l.logf("%v", strings.ToUpper(e.Signal.String())) case *Stopped: if e.Err != nil { l.logf("ERROR\t\tFailed to stop cleanly: %+v", e.Err) } case *RollingBack: l.logf("ERROR\t\tStart failed, rolling back: %+v", e.StartErr) case *RolledBack: if e.Err != nil { l.logf("ERROR\t\tCouldn't roll back cleanly: %+v", e.Err) } case *Started: if e.Err != nil { l.logf("ERROR\t\tFailed to start: %+v", e.Err) } else { l.logf("RUNNING") } case *LoggerInitialized: if e.Err != nil { l.logf("ERROR\t\tFailed to initialize custom logger: %+v", e.Err) } else { l.logf("LOGGER\tInitialized custom logger from %v", e.ConstructorName) } } } ================================================ FILE: fxevent/console_test.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxevent import ( "bytes" "errors" "fmt" "io" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" ) // richError prints a different output when formatted with %+v vs %v. type richError struct{} func (e *richError) Error() string { return "plain error" } func (e *richError) Format(w fmt.State, c rune) { if w.Flag('+') && c == 'v' { // Format differently for %+v. io.WriteString(w, "rich error") } else { io.WriteString(w, e.Error()) } } func TestConsoleLogger(t *testing.T) { t.Parallel() tests := []struct { name string give Event want string }{ { name: "OnStart executing", give: &OnStartExecuting{ FunctionName: "hook.onStart", CallerName: "bytes.NewBuffer", }, want: "[Fx] HOOK OnStart hook.onStart executing (caller: bytes.NewBuffer)\n", }, { name: "OnStopExecuting", give: &OnStopExecuting{ FunctionName: "hook.onStop1", CallerName: "bytes.NewBuffer", }, want: "[Fx] HOOK OnStop hook.onStop1 executing (caller: bytes.NewBuffer)\n", }, { name: "OnStopExecutedError", give: &OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }, want: "[Fx] HOOK OnStop hook.onStart1 called by bytes.NewBuffer failed in 0s: some error\n", }, { name: "OnStopExecutedError/rich error", give: &OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: &richError{}, }, want: "[Fx] HOOK OnStop hook.onStart1 called by bytes.NewBuffer failed in 0s: rich error\n", }, { name: "OnStopExecuted", give: &OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Runtime: time.Millisecond * 3, }, want: "[Fx] HOOK OnStop hook.onStart1 called by bytes.NewBuffer ran successfully in 3ms\n", }, { name: "OnStartExecutedError", give: &OnStartExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }, want: "[Fx] HOOK OnStart hook.onStart1 called by bytes.NewBuffer failed in 0s: some error\n", }, { name: "OnStartExecutedError/rich error", give: &OnStartExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: &richError{}, }, want: "[Fx] HOOK OnStart hook.onStart1 called by bytes.NewBuffer failed in 0s: rich error\n", }, { name: "OnStartExecuted", give: &OnStartExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Runtime: time.Millisecond * 3, }, want: "[Fx] HOOK OnStart hook.onStart1 called by bytes.NewBuffer ran successfully in 3ms\n", }, { name: "ProvideError", give: &Provided{Err: errors.New("some error")}, want: "[Fx] Error after options were applied: some error\n", }, { name: "ProvideError/rich error", give: &Provided{Err: &richError{}}, want: "[Fx] Error after options were applied: rich error\n", }, { name: "Supplied", give: &Supplied{ TypeName: "*bytes.Buffer", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, }, want: "[Fx] SUPPLY *bytes.Buffer\n", }, { name: "Supplied with module", give: &Supplied{ TypeName: "*bytes.Buffer", ModuleName: "myModule", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main", "mypackage.GetMyModule"}, }, want: "[Fx] SUPPLY *bytes.Buffer from module \"myModule\"\n", }, { name: "SuppliedError", give: &Supplied{TypeName: "*bytes.Buffer", Err: errors.New("great sadness")}, want: "[Fx] ERROR Failed to supply *bytes.Buffer: great sadness\n", }, { name: "SuppliedError/rich error", give: &Supplied{TypeName: "*bytes.Buffer", Err: &richError{}}, want: "[Fx] ERROR Failed to supply *bytes.Buffer: rich error\n", }, { name: "Provided", give: &Provided{ ConstructorName: "bytes.NewBuffer()", OutputTypeNames: []string{"*bytes.Buffer"}, Private: false, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, }, want: "[Fx] PROVIDE *bytes.Buffer <= bytes.NewBuffer()\n", }, { name: "Provided privately", give: &Provided{ ConstructorName: "bytes.NewBuffer()", OutputTypeNames: []string{"*bytes.Buffer"}, Private: true, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, }, want: "[Fx] PROVIDE (PRIVATE) *bytes.Buffer <= bytes.NewBuffer()\n", }, { name: "Provided with module", give: &Provided{ ConstructorName: "bytes.NewBuffer()", ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, Private: false, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main", "mypackage.GetMyModule"}, }, want: "[Fx] PROVIDE *bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n", }, { name: "Provided with module privately", give: &Provided{ ConstructorName: "bytes.NewBuffer()", ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, Private: true, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main", "mypackage.GetMyModule"}, }, want: "[Fx] PROVIDE (PRIVATE) *bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n", }, { name: "Replaced", give: &Replaced{ OutputTypeNames: []string{"*bytes.Buffer"}, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, }, want: "[Fx] REPLACE *bytes.Buffer\n", }, { name: "Replaced with module", give: &Replaced{ ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main", "mypackage.GetMyModule"}, }, want: "[Fx] REPLACE *bytes.Buffer from module \"myModule\"\n", }, { name: "ReplacedError", give: &Replaced{Err: errors.New("some error")}, want: "[Fx] ERROR Failed to replace: some error\n", }, { name: "Decorated", give: &Decorated{ DecoratorName: "bytes.NewBuffer()", OutputTypeNames: []string{"*bytes.Buffer"}, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, }, want: "[Fx] DECORATE *bytes.Buffer <= bytes.NewBuffer()\n", }, { name: "Decorated with module", give: &Decorated{ DecoratorName: "bytes.NewBuffer()", ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main", "mypackage.GetMyModule"}, }, want: "[Fx] DECORATE *bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n", }, { name: "DecorateError", give: &Decorated{Err: errors.New("some error")}, want: "[Fx] Error after options were applied: some error\n", }, { name: "DecorateError/rich error", give: &Decorated{Err: &richError{}}, want: "[Fx] Error after options were applied: rich error\n", }, { name: "Run", give: &Run{Name: "bytes.NewBuffer()", Kind: "constructor", Runtime: 10 * time.Nanosecond}, want: "[Fx] RUN\tconstructor: bytes.NewBuffer() in 10ns\n", }, { name: "Run with module", give: &Run{ Name: "bytes.NewBuffer()", Kind: "constructor", Runtime: 50 * time.Millisecond, ModuleName: "myModule", }, want: "[Fx] RUN\tconstructor: bytes.NewBuffer() in 50ms from module \"myModule\"\n", }, { name: "RunError", give: &Run{ Name: "bytes.NewBuffer()", Kind: "constructor", Runtime: 5 * time.Second, Err: errors.New("terrible constructor error"), }, want: joinLines( "[Fx] RUN\tconstructor: bytes.NewBuffer() in 5s", "[Fx] Error returned: terrible constructor error", ), }, { name: "Invoking", give: &Invoking{FunctionName: "bytes.NewBuffer()"}, want: "[Fx] INVOKE bytes.NewBuffer()\n", }, { name: "Invoking with module", give: &Invoking{ FunctionName: "bytes.NewBuffer()", ModuleName: "myModule", }, want: "[Fx] INVOKE bytes.NewBuffer() from module \"myModule\"\n", }, { name: "Invoked/Error", give: &Invoked{ FunctionName: "bytes.NewBuffer()", Err: errors.New("some error"), Trace: "foo()\n\tbar/baz.go:42\n", }, want: joinLines( "[Fx] ERROR fx.Invoke(bytes.NewBuffer()) called from:", "foo()", " bar/baz.go:42", "Failed: some error", ), }, { name: "Invoked/Error/rich", give: &Invoked{ FunctionName: "bytes.NewBuffer()", Err: &richError{}, Trace: "foo()\n\tbar/baz.go:42\n", }, want: joinLines( "[Fx] ERROR fx.Invoke(bytes.NewBuffer()) called from:", "foo()", " bar/baz.go:42", "Failed: rich error", ), }, { name: "StartError", give: &Started{Err: errors.New("some error")}, want: "[Fx] ERROR Failed to start: some error\n", }, { name: "StartError/rich error", give: &Started{Err: &richError{}}, want: "[Fx] ERROR Failed to start: rich error\n", }, { name: "Stopping", give: &Stopping{Signal: os.Interrupt}, want: "[Fx] INTERRUPT\n", }, { name: "Stopped", give: &Stopped{Err: errors.New("some error")}, want: "[Fx] ERROR Failed to stop cleanly: some error\n", }, { name: "Stopped/rich error", give: &Stopped{Err: &richError{}}, want: "[Fx] ERROR Failed to stop cleanly: rich error\n", }, { name: "RollingBack", give: &RollingBack{StartErr: errors.New("some error")}, want: "[Fx] ERROR Start failed, rolling back: some error\n", }, { name: "RollingBack/rich error", give: &RollingBack{StartErr: &richError{}}, want: "[Fx] ERROR Start failed, rolling back: rich error\n", }, { name: "RolledBack", give: &RolledBack{Err: errors.New("some error")}, want: "[Fx] ERROR Couldn't roll back cleanly: some error\n", }, { name: "RolledBack/rich error", give: &RolledBack{Err: &richError{}}, want: "[Fx] ERROR Couldn't roll back cleanly: rich error\n", }, { name: "Started", give: &Started{}, want: "[Fx] RUNNING\n", }, { name: "CustomLoggerError", give: &LoggerInitialized{Err: errors.New("great sadness")}, want: "[Fx] ERROR Failed to initialize custom logger: great sadness\n", }, { name: "CustomLoggerError/rich error", give: &LoggerInitialized{Err: &richError{}}, want: "[Fx] ERROR Failed to initialize custom logger: rich error\n", }, { name: "LoggerInitialized", give: &LoggerInitialized{ConstructorName: "go.uber.org/fx/fxevent.TestConsoleLogger.func1()"}, want: "[Fx] LOGGER Initialized custom logger from go.uber.org/fx/fxevent.TestConsoleLogger.func1()\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer (&ConsoleLogger{W: &buff}).LogEvent(tt.give) assert.Equal(t, tt.want, buff.String()) }) } } func joinLines(lines ...string) string { return strings.Join(lines, "\n") + "\n" } ================================================ FILE: fxevent/doc.go ================================================ // Copyright (c) 2024 Uber Technologies, Inc. // // 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. // Package fxevent defines a means of changing how Fx logs internal events. // // # Changing the Logger // // By default, the [ConsoleLogger] is used, writing readable logs to stderr. // // You can use the fx.WithLogger option to change this behavior // by providing a constructor that returns an alternative implementation // of the [Logger] interface. // // fx.WithLogger(func(cfg *Config) Logger { // return &MyFxLogger{...} // }) // // Because WithLogger accepts a constructor, // you can pull any other types and values from inside the container, // allowing use of the same logger that your application uses. // // For example, if you're using Zap inside your application, // you can use the [ZapLogger] implementation of the interface. // // fx.New( // fx.Provide( // zap.NewProduction, // provide a *zap.Logger // ), // fx.WithLogger( // func(log *zap.Logger) fxevent.Logger { // return &fxevent.ZapLogger{Logger: log} // }, // ), // ) // // # Implementing a Custom Logger // // To implement a custom logger, you need to implement the [Logger] interface. // The Logger.LogEvent method accepts an [Event] object. // // [Event] is a union type that represents all the different events that Fx can emit. // You can use a type switch to handle each event type. // See 'event.go' for a list of all the possible events. // // func (l *MyFxLogger) LogEvent(e fxevent.Event) { // switch e := e.(type) { // case *fxevent.OnStartExecuting: // // ... // // ... // } // } // // The events contain enough information for observability and debugging purposes. // If you need more information in them, // feel free to open an issue to discuss the addition. package fxevent ================================================ FILE: fxevent/event.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxevent import ( "os" "time" ) // Event defines an event emitted by fx. type Event interface { event() // Only fxlog can implement this interface. } // Passing events by type to make Event hashable in the future. func (*OnStartExecuting) event() {} func (*OnStartExecuted) event() {} func (*OnStopExecuting) event() {} func (*OnStopExecuted) event() {} func (*Supplied) event() {} func (*Provided) event() {} func (*Replaced) event() {} func (*Decorated) event() {} func (*BeforeRun) event() {} func (*Run) event() {} func (*Invoking) event() {} func (*Invoked) event() {} func (*Stopping) event() {} func (*Stopped) event() {} func (*RollingBack) event() {} func (*RolledBack) event() {} func (*Started) event() {} func (*LoggerInitialized) event() {} // OnStartExecuting is emitted before an OnStart hook is executed. type OnStartExecuting struct { // FunctionName is the name of the function that will be executed. FunctionName string // CallerName is the name of the function that scheduled the hook for // execution. CallerName string } // OnStartExecuted is emitted after an OnStart hook has been executed. type OnStartExecuted struct { // FunctionName is the name of the function that was executed. FunctionName string // CallerName is the name of the function that scheduled the hook for // execution. CallerName string // Method specifies the kind of the hook. This is one of "OnStart" and // "OnStop". Method string // Runtime specifies how long it took to run this hook. Runtime time.Duration // Err is non-nil if the hook failed to execute. Err error } // OnStopExecuting is emitted before an OnStop hook is executed. type OnStopExecuting struct { // FunctionName is the name of the function that will be executed. FunctionName string // CallerName is the name of the function that scheduled the hook for // execution. CallerName string } // OnStopExecuted is emitted after an OnStop hook has been executed. type OnStopExecuted struct { // FunctionName is the name of the function that was executed. FunctionName string // CallerName is the name of the function that scheduled the hook for // execution. CallerName string // Runtime specifies how long it took to run this hook. Runtime time.Duration // Err is non-nil if the hook failed to execute. Err error } // Supplied is emitted after a value is added with fx.Supply. type Supplied struct { // TypeName is the name of the type of value that was added. TypeName string // StackTrace is the stack trace of the call to Supply. StackTrace []string // ModuleTrace contains the module locations through which this value was added. ModuleTrace []string // ModuleName is the name of the module in which the value was added to. ModuleName string // Err is non-nil if we failed to supply the value. Err error } // Provided is emitted when a constructor is provided to Fx. type Provided struct { // ConstructorName is the name of the constructor that was provided to // Fx. ConstructorName string // StackTrace is the stack trace of where the constructor was provided to Fx. StackTrace []string // ModuleTrace contains the module locations through which this was provided to Fx. ModuleTrace []string // OutputTypeNames is a list of names of types that are produced by // this constructor. OutputTypeNames []string // ModuleName is the name of the module in which the constructor was // provided to. ModuleName string // Err is non-nil if we failed to provide this constructor. Err error // Private denotes whether the provided constructor is a [Private] constructor. Private bool } // Replaced is emitted when a value replaces a type in Fx. type Replaced struct { // OutputTypeNames is a list of names of types that were replaced. OutputTypeNames []string // StackTrace is the stack trace of the call to Replace. StackTrace []string // ModuleTrace contains the module locations through which this value was added. ModuleTrace []string // ModuleName is the name of the module in which the value was added to. ModuleName string // Err is non-nil if we failed to supply the value. Err error } // Decorated is emitted when a decorator is executed in Fx. type Decorated struct { // DecoratorName is the name of the decorator function that was // provided to Fx. DecoratorName string // StackTrace is the stack trace of where the decorator was given to Fx. StackTrace []string // ModuleTrace contains the module locations through which this value was added. ModuleTrace []string // ModuleName is the name of the module in which the value was added to. ModuleName string // OutputTypeNames is a list of names of types that are decorated by // this decorator. OutputTypeNames []string // Err is non-nil if we failed to run this decorator. Err error } // BeforeRun is emitted before a constructor, decorator, or supply/replace stub is run by Fx. // When complete, a Run will be emitted. type BeforeRun struct { // Name is the name of the function that will be run. Name string // Kind indicates which Fx option was used to pass along the function. // It is either "provide", "decorate", "supply", or "replace". Kind string // ModuleName is the name of the module in which the function belongs. ModuleName string } // Run is emitted after a constructor, decorator, or supply/replace stub is run by Fx. type Run struct { // Name is the name of the function that was run. Name string // Kind indicates which Fx option was used to pass along the function. // It is either "provide", "decorate", "supply", or "replace". Kind string // ModuleName is the name of the module in which the function belongs. ModuleName string // Runtime specifies how long it took to run this function. Runtime time.Duration // Err is non-nil if the function returned an error. // If fx.RecoverFromPanics is used, this will include panics. Err error } // Invoking is emitted before we invoke a function specified with fx.Invoke. type Invoking struct { // FunctionName is the name of the function that will be invoked. FunctionName string // ModuleName is the name of the module in which the value was added to. ModuleName string } // Invoked is emitted after we invoke a function specified with fx.Invoke, // whether it succeeded or failed. type Invoked struct { // Functionname is the name of the function that was invoked. FunctionName string // ModuleName is the name of the module in which the value was added to. ModuleName string // Err is non-nil if the function failed to execute. Err error // Trace records information about where the fx.Invoke call was made. // Note that this is NOT a stack trace of the error itself. Trace string } // Started is emitted when an application is started successfully and/or it // errored. type Started struct { // Err is non-nil if the application failed to start successfully. Err error } // Stopping is emitted when the application receives a signal to shut down // after starting. This may happen with fx.Shutdowner or by sending a signal to // the application on the command line. type Stopping struct { // Signal is the signal that caused this shutdown. Signal os.Signal } // Stopped is emitted when the application has finished shutting down, whether // successfully or not. type Stopped struct { // Err is non-nil if errors were encountered during shutdown. Err error } // RollingBack is emitted when the application failed to start up due to an // error, and is being rolled back. type RollingBack struct { // StartErr is the error that caused this rollback. StartErr error } // RolledBack is emitted after a service has been rolled back, whether it // succeeded or not. type RolledBack struct { // Err is non-nil if the rollback failed. Err error } // LoggerInitialized is emitted when a logger supplied with fx.WithLogger is // instantiated, or if it fails to instantiate. type LoggerInitialized struct { // ConstructorName is the name of the constructor that builds this // logger. ConstructorName string // Err is non-nil if the logger failed to build. Err error } ================================================ FILE: fxevent/event_test.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxevent import ( "testing" "go.uber.org/goleak" ) // TestForCoverage adds coverage for own sake. func TestForCoverage(t *testing.T) { t.Parallel() events := []Event{ &OnStartExecuting{}, &OnStartExecuted{}, &OnStopExecuting{}, &OnStopExecuted{}, &Supplied{}, &Provided{}, &Replaced{}, &Decorated{}, &BeforeRun{}, &Run{}, &Invoking{}, &Invoked{}, &Stopping{}, &Stopped{}, &RollingBack{}, &RolledBack{}, &Started{}, &LoggerInitialized{}, } for _, e := range events { e.event() } } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ================================================ FILE: fxevent/logger.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxevent // Logger defines interface used for logging. type Logger interface { // LogEvent is called when a logging event is emitted. LogEvent(Event) } // NopLogger is an Fx event logger that ignores all messages. var NopLogger = nopLogger{} type nopLogger struct{} var _ Logger = nopLogger{} func (nopLogger) LogEvent(Event) {} func (nopLogger) String() string { return "NopLogger" } ================================================ FILE: fxevent/slog.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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:build go1.21 package fxevent import ( "context" "log/slog" "strconv" "strings" ) var _ Logger = (*SlogLogger)(nil) // SlogLogger an Fx event logger that logs events using a slog logger. type SlogLogger struct { Logger *slog.Logger ctx context.Context logLevel slog.Level errorLevel *slog.Level } // UseContext sets the context that will be used when logging to slog. func (l *SlogLogger) UseContext(ctx context.Context) { l.ctx = ctx } // UseLogLevel sets the level of non-error logs emitted by Fx to level. func (l *SlogLogger) UseLogLevel(level slog.Level) { l.logLevel = level } // UseErrorLevel sets the level of error logs emitted by Fx to level. func (l *SlogLogger) UseErrorLevel(level slog.Level) { l.errorLevel = &level } func (l *SlogLogger) filter(fields []any) []any { filtered := []any{} for _, field := range fields { if field, ok := field.(slog.Attr); ok { if _, ok := field.Value.Any().(slogFieldSkip); ok { continue } } filtered = append(filtered, field) } return filtered } func (l *SlogLogger) logEvent(msg string, fields ...any) { l.Logger.Log(l.ctx, l.logLevel, msg, l.filter(fields)...) } func (l *SlogLogger) logError(msg string, fields ...any) { lvl := slog.LevelError if l.errorLevel != nil { lvl = *l.errorLevel } l.Logger.Log(l.ctx, lvl, msg, l.filter(fields)...) } // LogEvent logs the given event to the provided Zap logger. func (l *SlogLogger) LogEvent(event Event) { switch e := event.(type) { case *OnStartExecuting: l.logEvent("OnStart hook executing", slog.String("callee", e.FunctionName), slog.String("caller", e.CallerName), ) case *OnStartExecuted: if e.Err != nil { l.logError("OnStart hook failed", slog.String("callee", e.FunctionName), slog.String("caller", e.CallerName), slogErr(e.Err), ) } else { l.logEvent("OnStart hook executed", slog.String("callee", e.FunctionName), slog.String("caller", e.CallerName), slog.String("runtime", e.Runtime.String()), ) } case *OnStopExecuting: l.logEvent("OnStop hook executing", slog.String("callee", e.FunctionName), slog.String("caller", e.CallerName), ) case *OnStopExecuted: if e.Err != nil { l.logError("OnStop hook failed", slog.String("callee", e.FunctionName), slog.String("caller", e.CallerName), slogErr(e.Err), ) } else { l.logEvent("OnStop hook executed", slog.String("callee", e.FunctionName), slog.String("caller", e.CallerName), slog.String("runtime", e.Runtime.String()), ) } case *Supplied: if e.Err != nil { l.logError("error encountered while applying options", slog.String("type", e.TypeName), slogStrings("moduletrace", e.ModuleTrace), slogStrings("stacktrace", e.StackTrace), slogMaybeModuleField(e.ModuleName), slogErr(e.Err)) } else { l.logEvent("supplied", slog.String("type", e.TypeName), slogStrings("stacktrace", e.StackTrace), slogStrings("moduletrace", e.ModuleTrace), slogMaybeModuleField(e.ModuleName), ) } case *Provided: for _, rtype := range e.OutputTypeNames { l.logEvent("provided", slog.String("constructor", e.ConstructorName), slogStrings("stacktrace", e.StackTrace), slogStrings("moduletrace", e.ModuleTrace), slogMaybeModuleField(e.ModuleName), slog.String("type", rtype), slogMaybeBool("private", e.Private), ) } if e.Err != nil { l.logError("error encountered while applying options", slogMaybeModuleField(e.ModuleName), slogStrings("stacktrace", e.StackTrace), slogStrings("moduletrace", e.ModuleTrace), slogErr(e.Err)) } case *Replaced: for _, rtype := range e.OutputTypeNames { l.logEvent("replaced", slogStrings("stacktrace", e.StackTrace), slogStrings("moduletrace", e.ModuleTrace), slogMaybeModuleField(e.ModuleName), slog.String("type", rtype), ) } if e.Err != nil { l.logError("error encountered while replacing", slogStrings("stacktrace", e.StackTrace), slogStrings("moduletrace", e.ModuleTrace), slogMaybeModuleField(e.ModuleName), slogErr(e.Err)) } case *Decorated: for _, rtype := range e.OutputTypeNames { l.logEvent("decorated", slog.String("decorator", e.DecoratorName), slogStrings("stacktrace", e.StackTrace), slogStrings("moduletrace", e.ModuleTrace), slogMaybeModuleField(e.ModuleName), slog.String("type", rtype), ) } if e.Err != nil { l.logError("error encountered while applying options", slogStrings("stacktrace", e.StackTrace), slogStrings("moduletrace", e.ModuleTrace), slogMaybeModuleField(e.ModuleName), slogErr(e.Err)) } case *BeforeRun: l.logEvent("before run", slog.String("name", e.Name), slog.String("kind", e.Kind), slogMaybeModuleField(e.ModuleName), ) case *Run: if e.Err != nil { l.logError("error returned", slog.String("name", e.Name), slog.String("kind", e.Kind), slogMaybeModuleField(e.ModuleName), slogErr(e.Err), ) } else { l.logEvent("run", slog.String("name", e.Name), slog.String("kind", e.Kind), slog.String("runtime", e.Runtime.String()), slogMaybeModuleField(e.ModuleName), ) } case *Invoking: // Do not log stack as it will make logs hard to read. l.logEvent("invoking", slog.String("function", e.FunctionName), slogMaybeModuleField(e.ModuleName), ) case *Invoked: if e.Err != nil { l.logError("invoke failed", slogErr(e.Err), slog.String("stack", e.Trace), slog.String("function", e.FunctionName), slogMaybeModuleField(e.ModuleName), ) } case *Stopping: l.logEvent("received signal", slog.String("signal", strings.ToUpper(e.Signal.String()))) case *Stopped: if e.Err != nil { l.logError("stop failed", slogErr(e.Err)) } case *RollingBack: l.logError("start failed, rolling back", slogErr(e.StartErr)) case *RolledBack: if e.Err != nil { l.logError("rollback failed", slogErr(e.Err)) } case *Started: if e.Err != nil { l.logError("start failed", slogErr(e.Err)) } else { l.logEvent("started") } case *LoggerInitialized: if e.Err != nil { l.logError("custom logger initialization failed", slogErr(e.Err)) } else { l.logEvent("initialized custom fxevent.Logger", slog.String("function", e.ConstructorName)) } } } type slogFieldSkip struct{} func slogMaybeModuleField(name string) slog.Attr { if len(name) == 0 { return slog.Any("module", slogFieldSkip{}) } return slog.String("module", name) } func slogMaybeBool(name string, b bool) slog.Attr { if !b { return slog.Any(name, slogFieldSkip{}) } return slog.Bool(name, true) } func slogErr(err error) slog.Attr { return slog.String("error", err.Error()) } func slogStrings(key string, str []string) slog.Attr { attrs := make([]any, len(str)) for i, val := range str { attrs[i] = slog.String(strconv.Itoa(i), val) } return slog.Group(key, attrs...) } ================================================ FILE: fxevent/slog_test.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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:build go1.21 package fxevent import ( "context" "errors" "fmt" "log/slog" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type slogObservableEntry struct { record slog.Record } func (s slogObservableEntry) unwrap(attr slog.Attr, out map[string]any) { anyAttr := attr.Value.Any() sliceAttr, ok := anyAttr.([]slog.Attr) if !ok { out[attr.Key] = anyAttr return } sliceAttrValues := make([]any, len(sliceAttr)) for i, iter := range sliceAttr { sliceAttrValues[i] = iter.Value.Any() } out[attr.Key] = sliceAttrValues } func (s slogObservableEntry) ContextMap() map[string]any { contextMap := map[string]any{} s.record.Attrs(func(a slog.Attr) bool { s.unwrap(a, contextMap) return true }) return contextMap } type slogObservableLogger struct { level slog.Level entries []slogObservableEntry attrs []slog.Attr } func (s *slogObservableLogger) Enabled(ctx context.Context, level slog.Level) bool { return int(s.level) <= int(level) } func (s *slogObservableLogger) Handle(ctx context.Context, record slog.Record) error { s.entries = append(s.entries, slogObservableEntry{record}) return nil } func (s *slogObservableLogger) WithAttrs(attrs []slog.Attr) slog.Handler { return &slogObservableLogger{ level: s.level, entries: s.entries, attrs: append(s.attrs, attrs...), } } func (s *slogObservableLogger) WithGroup(name string) slog.Handler { return s } func (s *slogObservableLogger) TakeAll() []slogObservableEntry { return s.entries } func newSlogObservableLogger(level slog.Level) (*slog.Logger, *slogObservableLogger) { handler := &slogObservableLogger{level: level} return slog.New(handler), handler } func TestSlogLogger(t *testing.T) { t.Parallel() someError := errors.New("some error") tests := []struct { name string give Event wantMessage string wantFields map[string]any }{ { name: "OnStartExecuting", give: &OnStartExecuting{ FunctionName: "hook.onStart", CallerName: "bytes.NewBuffer", }, wantMessage: "OnStart hook executing", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart", }, }, { name: "OnStopExecuting", give: &OnStopExecuting{ FunctionName: "hook.onStop1", CallerName: "bytes.NewBuffer", }, wantMessage: "OnStop hook executing", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStop1", }, }, { name: "OnStopExecuted/Error", give: &OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }, wantMessage: "OnStop hook failed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "error": "some error", }, }, { name: "OnStopExecuted", give: &OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Runtime: time.Millisecond * 3, }, wantMessage: "OnStop hook executed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "runtime": "3ms", }, }, { name: "OnStartExecuted/Error", give: &OnStartExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }, wantMessage: "OnStart hook failed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "error": "some error", }, }, { name: "OnStartExecuted", give: &OnStartExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Runtime: time.Millisecond * 3, }, wantMessage: "OnStart hook executed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "runtime": "3ms", }, }, { name: "Supplied", give: &Supplied{ TypeName: "*bytes.Buffer", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, }, wantMessage: "supplied", wantFields: map[string]any{ "type": "*bytes.Buffer", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, }, }, { name: "Supplied/Error", give: &Supplied{ TypeName: "*bytes.Buffer", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while applying options", wantFields: map[string]any{ "type": "*bytes.Buffer", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "Provide", give: &Provided{ ConstructorName: "bytes.NewBuffer()", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, Private: false, }, wantMessage: "provided", wantFields: map[string]any{ "constructor": "bytes.NewBuffer()", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "type": "*bytes.Buffer", "module": "myModule", }, }, { name: "PrivateProvide", give: &Provided{ ConstructorName: "bytes.NewBuffer()", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, Private: true, }, wantMessage: "provided", wantFields: map[string]any{ "constructor": "bytes.NewBuffer()", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "type": "*bytes.Buffer", "module": "myModule", "private": true, }, }, { name: "Provide/Error", give: &Provided{ StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while applying options", wantFields: map[string]any{ "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "Replace", give: &Replaced{ ModuleName: "myModule", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, OutputTypeNames: []string{"*bytes.Buffer"}, }, wantMessage: "replaced", wantFields: map[string]any{ "type": "*bytes.Buffer", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "module": "myModule", }, }, { name: "Replace/Error", give: &Replaced{ StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while replacing", wantFields: map[string]any{ "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "Decorate", give: &Decorated{ DecoratorName: "bytes.NewBuffer()", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, }, wantMessage: "decorated", wantFields: map[string]any{ "decorator": "bytes.NewBuffer()", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "type": "*bytes.Buffer", "module": "myModule", }, }, { name: "Decorate/Error", give: &Decorated{ StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while applying options", wantFields: map[string]any{ "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "BeforeRun", give: &BeforeRun{Name: "bytes.NewBuffer()", Kind: "constructor"}, wantMessage: "before run", wantFields: map[string]any{ "name": "bytes.NewBuffer()", "kind": "constructor", }, }, { name: "Run", give: &Run{Name: "bytes.NewBuffer()", Kind: "constructor", Runtime: 3 * time.Millisecond}, wantMessage: "run", wantFields: map[string]any{ "name": "bytes.NewBuffer()", "kind": "constructor", "runtime": "3ms", }, }, { name: "Run with module", give: &Run{ Name: "bytes.NewBuffer()", Kind: "constructor", ModuleName: "myModule", Runtime: 3 * time.Millisecond, }, wantMessage: "run", wantFields: map[string]any{ "name": "bytes.NewBuffer()", "kind": "constructor", "module": "myModule", "runtime": "3ms", }, }, { name: "Run/Error", give: &Run{ Name: "bytes.NewBuffer()", Kind: "constructor", Err: someError, }, wantMessage: "error returned", wantFields: map[string]any{ "name": "bytes.NewBuffer()", "kind": "constructor", "error": "some error", }, }, { name: "Invoking/Success", give: &Invoking{ModuleName: "myModule", FunctionName: "bytes.NewBuffer()"}, wantMessage: "invoking", wantFields: map[string]any{ "function": "bytes.NewBuffer()", "module": "myModule", }, }, { name: "Invoked/Error", give: &Invoked{FunctionName: "bytes.NewBuffer()", Err: someError}, wantMessage: "invoke failed", wantFields: map[string]any{ "error": "some error", "stack": "", "function": "bytes.NewBuffer()", }, }, { name: "Start/Error", give: &Started{Err: someError}, wantMessage: "start failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "Stopping", give: &Stopping{Signal: os.Interrupt}, wantMessage: "received signal", wantFields: map[string]any{ "signal": "INTERRUPT", }, }, { name: "Stopped/Error", give: &Stopped{Err: someError}, wantMessage: "stop failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "RollingBack/Error", give: &RollingBack{StartErr: someError}, wantMessage: "start failed, rolling back", wantFields: map[string]any{ "error": "some error", }, }, { name: "RolledBack/Error", give: &RolledBack{Err: someError}, wantMessage: "rollback failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "Started", give: &Started{}, wantMessage: "started", wantFields: map[string]any{}, }, { name: "LoggerInitialized/Error", give: &LoggerInitialized{Err: someError}, wantMessage: "custom logger initialization failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "LoggerInitialized", give: &LoggerInitialized{ConstructorName: "bytes.NewBuffer()"}, wantMessage: "initialized custom fxevent.Logger", wantFields: map[string]any{ "function": "bytes.NewBuffer()", }, }, } t.Run("debug observer, log at default (info)", func(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() core, observedLogs := newSlogObservableLogger(slog.LevelDebug) (&SlogLogger{Logger: core}).LogEvent(tt.give) logs := observedLogs.TakeAll() require.Len(t, logs, 1) got := logs[0] assert.Equal(t, tt.wantMessage, got.record.Message) assert.Equal(t, tt.wantFields, got.ContextMap()) }) } }) t.Run("info observer, log at debug", func(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() core, observedLogs := newSlogObservableLogger(slog.LevelInfo) l := &SlogLogger{Logger: core} l.UseLogLevel(slog.LevelDebug) l.LogEvent(tt.give) logs := observedLogs.TakeAll() // logs are not visible unless they are errors if strings.HasSuffix(tt.name, "/Error") { require.Len(t, logs, 1) got := logs[0] assert.Equal(t, tt.wantMessage, got.record.Message) assert.Equal(t, tt.wantFields, got.ContextMap()) } else { require.Len(t, logs, 0) } }) } }) t.Run("info observer, log/error at debug", func(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() core, observedLogs := newSlogObservableLogger(slog.LevelInfo) l := &SlogLogger{Logger: core} l.UseLogLevel(slog.LevelDebug) l.UseErrorLevel(slog.LevelDebug) l.LogEvent(tt.give) logs := observedLogs.TakeAll() require.Len(t, logs, 0, "no logs should be visible") }) } }) t.Run("test setting log levels", func(t *testing.T) { levels := []slog.Level{ slog.LevelError, slog.LevelDebug, slog.LevelWarn, slog.LevelInfo, } for _, level := range levels { core, observedLogs := newSlogObservableLogger(level) logger := &SlogLogger{Logger: core} logger.UseLogLevel(level) func() { defer func() { recover() }() logger.LogEvent(&OnStartExecuting{ FunctionName: "hook.onStart", CallerName: "bytes.NewBuffer", }) }() logs := observedLogs.TakeAll() require.Len(t, logs, 1) } }) t.Run("test setting error log levels", func(t *testing.T) { levels := []slog.Level{ slog.LevelError, slog.LevelDebug, slog.LevelWarn, slog.LevelInfo, } for _, level := range levels { core, observedLogs := newSlogObservableLogger(level) logger := &SlogLogger{Logger: core} logger.UseErrorLevel(level) func() { defer func() { recover() }() logger.LogEvent(&OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }) }() logs := observedLogs.TakeAll() require.Len(t, logs, 1) } }) } ================================================ FILE: fxevent/zap.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxevent import ( "strings" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // ZapLogger is an Fx event logger that logs events to Zap. type ZapLogger struct { Logger *zap.Logger logLevel zapcore.Level // default: zapcore.InfoLevel errorLevel *zapcore.Level } var _ Logger = (*ZapLogger)(nil) // UseErrorLevel sets the level of error logs emitted by Fx to level. func (l *ZapLogger) UseErrorLevel(level zapcore.Level) { l.errorLevel = &level } // UseLogLevel sets the level of non-error logs emitted by Fx to level. func (l *ZapLogger) UseLogLevel(level zapcore.Level) { l.logLevel = level } func (l *ZapLogger) logEvent(msg string, fields ...zap.Field) { l.Logger.Log(l.logLevel, msg, fields...) } func (l *ZapLogger) logError(msg string, fields ...zap.Field) { lvl := zapcore.ErrorLevel if l.errorLevel != nil { lvl = *l.errorLevel } l.Logger.Log(lvl, msg, fields...) } // LogEvent logs the given event to the provided Zap logger. func (l *ZapLogger) LogEvent(event Event) { switch e := event.(type) { case *OnStartExecuting: l.logEvent("OnStart hook executing", zap.String("callee", e.FunctionName), zap.String("caller", e.CallerName), ) case *OnStartExecuted: if e.Err != nil { l.logError("OnStart hook failed", zap.String("callee", e.FunctionName), zap.String("caller", e.CallerName), zap.Error(e.Err), ) } else { l.logEvent("OnStart hook executed", zap.String("callee", e.FunctionName), zap.String("caller", e.CallerName), zap.String("runtime", e.Runtime.String()), ) } case *OnStopExecuting: l.logEvent("OnStop hook executing", zap.String("callee", e.FunctionName), zap.String("caller", e.CallerName), ) case *OnStopExecuted: if e.Err != nil { l.logError("OnStop hook failed", zap.String("callee", e.FunctionName), zap.String("caller", e.CallerName), zap.Error(e.Err), ) } else { l.logEvent("OnStop hook executed", zap.String("callee", e.FunctionName), zap.String("caller", e.CallerName), zap.String("runtime", e.Runtime.String()), ) } case *Supplied: if e.Err != nil { l.logError("error encountered while applying options", zap.String("type", e.TypeName), zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), moduleField(e.ModuleName), zap.Error(e.Err)) } else { l.logEvent("supplied", zap.String("type", e.TypeName), zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), moduleField(e.ModuleName), ) } case *Provided: for _, rtype := range e.OutputTypeNames { l.logEvent("provided", zap.String("constructor", e.ConstructorName), zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), moduleField(e.ModuleName), zap.String("type", rtype), maybeBool("private", e.Private), ) } if e.Err != nil { l.logError("error encountered while applying options", moduleField(e.ModuleName), zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), zap.Error(e.Err)) } case *Replaced: for _, rtype := range e.OutputTypeNames { l.logEvent("replaced", zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), moduleField(e.ModuleName), zap.String("type", rtype), ) } if e.Err != nil { l.logError("error encountered while replacing", zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), moduleField(e.ModuleName), zap.Error(e.Err)) } case *Decorated: for _, rtype := range e.OutputTypeNames { l.logEvent("decorated", zap.String("decorator", e.DecoratorName), zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), moduleField(e.ModuleName), zap.String("type", rtype), ) } if e.Err != nil { l.logError("error encountered while applying options", zap.Strings("stacktrace", e.StackTrace), zap.Strings("moduletrace", e.ModuleTrace), moduleField(e.ModuleName), zap.Error(e.Err)) } case *BeforeRun: l.logEvent("before run", zap.String("name", e.Name), zap.String("kind", e.Kind), moduleField(e.ModuleName), ) case *Run: if e.Err != nil { l.logError("error returned", zap.String("name", e.Name), zap.String("kind", e.Kind), moduleField(e.ModuleName), zap.Error(e.Err), ) } else { l.logEvent("run", zap.String("name", e.Name), zap.String("kind", e.Kind), zap.String("runtime", e.Runtime.String()), moduleField(e.ModuleName), ) } case *Invoking: // Do not log stack as it will make logs hard to read. l.logEvent("invoking", zap.String("function", e.FunctionName), moduleField(e.ModuleName), ) case *Invoked: if e.Err != nil { l.logError("invoke failed", zap.Error(e.Err), zap.String("stack", e.Trace), zap.String("function", e.FunctionName), moduleField(e.ModuleName), ) } case *Stopping: l.logEvent("received signal", zap.String("signal", strings.ToUpper(e.Signal.String()))) case *Stopped: if e.Err != nil { l.logError("stop failed", zap.Error(e.Err)) } case *RollingBack: l.logError("start failed, rolling back", zap.Error(e.StartErr)) case *RolledBack: if e.Err != nil { l.logError("rollback failed", zap.Error(e.Err)) } case *Started: if e.Err != nil { l.logError("start failed", zap.Error(e.Err)) } else { l.logEvent("started") } case *LoggerInitialized: if e.Err != nil { l.logError("custom logger initialization failed", zap.Error(e.Err)) } else { l.logEvent("initialized custom fxevent.Logger", zap.String("function", e.ConstructorName)) } } } func moduleField(name string) zap.Field { if len(name) == 0 { return zap.Skip() } return zap.String("module", name) } func maybeBool(name string, b bool) zap.Field { if b { return zap.Bool(name, true) } return zap.Skip() } ================================================ FILE: fxevent/zap_test.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxevent import ( "errors" "fmt" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" ) func TestZapLogger(t *testing.T) { t.Parallel() someError := errors.New("some error") tests := []struct { name string give Event wantMessage string wantFields map[string]any }{ { name: "OnStartExecuting", give: &OnStartExecuting{ FunctionName: "hook.onStart", CallerName: "bytes.NewBuffer", }, wantMessage: "OnStart hook executing", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart", }, }, { name: "OnStopExecuting", give: &OnStopExecuting{ FunctionName: "hook.onStop1", CallerName: "bytes.NewBuffer", }, wantMessage: "OnStop hook executing", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStop1", }, }, { name: "OnStopExecuted/Error", give: &OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }, wantMessage: "OnStop hook failed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "error": "some error", }, }, { name: "OnStopExecuted", give: &OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Runtime: time.Millisecond * 3, }, wantMessage: "OnStop hook executed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "runtime": "3ms", }, }, { name: "OnStartExecuted/Error", give: &OnStartExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }, wantMessage: "OnStart hook failed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "error": "some error", }, }, { name: "OnStartExecuted", give: &OnStartExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Runtime: time.Millisecond * 3, }, wantMessage: "OnStart hook executed", wantFields: map[string]any{ "caller": "bytes.NewBuffer", "callee": "hook.onStart1", "runtime": "3ms", }, }, { name: "Supplied", give: &Supplied{ TypeName: "*bytes.Buffer", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, }, wantMessage: "supplied", wantFields: map[string]any{ "type": "*bytes.Buffer", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, }, }, { name: "Supplied/Error", give: &Supplied{ TypeName: "*bytes.Buffer", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while applying options", wantFields: map[string]any{ "type": "*bytes.Buffer", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "Provide", give: &Provided{ ConstructorName: "bytes.NewBuffer()", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, Private: false, }, wantMessage: "provided", wantFields: map[string]any{ "constructor": "bytes.NewBuffer()", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "type": "*bytes.Buffer", "module": "myModule", }, }, { name: "PrivateProvide", give: &Provided{ ConstructorName: "bytes.NewBuffer()", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, Private: true, }, wantMessage: "provided", wantFields: map[string]any{ "constructor": "bytes.NewBuffer()", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "type": "*bytes.Buffer", "module": "myModule", "private": true, }, }, { name: "Provide/Error", give: &Provided{ StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while applying options", wantFields: map[string]any{ "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "Replace", give: &Replaced{ ModuleName: "myModule", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, OutputTypeNames: []string{"*bytes.Buffer"}, }, wantMessage: "replaced", wantFields: map[string]any{ "type": "*bytes.Buffer", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "module": "myModule", }, }, { name: "Replace/Error", give: &Replaced{ StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while replacing", wantFields: map[string]any{ "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "Decorate", give: &Decorated{ DecoratorName: "bytes.NewBuffer()", StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, ModuleName: "myModule", OutputTypeNames: []string{"*bytes.Buffer"}, }, wantMessage: "decorated", wantFields: map[string]any{ "decorator": "bytes.NewBuffer()", "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "type": "*bytes.Buffer", "module": "myModule", }, }, { name: "Decorate/Error", give: &Decorated{ StackTrace: []string{"main.main", "runtime.main"}, ModuleTrace: []string{"main.main"}, Err: someError, }, wantMessage: "error encountered while applying options", wantFields: map[string]any{ "stacktrace": []any{"main.main", "runtime.main"}, "moduletrace": []any{"main.main"}, "error": "some error", }, }, { name: "Run", give: &Run{Name: "bytes.NewBuffer()", Kind: "constructor", Runtime: time.Second}, wantMessage: "run", wantFields: map[string]any{ "name": "bytes.NewBuffer()", "kind": "constructor", "runtime": "1s", }, }, { name: "Run with module", give: &Run{ Name: "bytes.NewBuffer()", Kind: "constructor", ModuleName: "myModule", Runtime: time.Millisecond, }, wantMessage: "run", wantFields: map[string]any{ "name": "bytes.NewBuffer()", "kind": "constructor", "module": "myModule", "runtime": "1ms", }, }, { name: "Run/Error", give: &Run{ Name: "bytes.NewBuffer()", Kind: "constructor", Err: someError, }, wantMessage: "error returned", wantFields: map[string]any{ "name": "bytes.NewBuffer()", "kind": "constructor", "error": "some error", }, }, { name: "Invoking/Success", give: &Invoking{ModuleName: "myModule", FunctionName: "bytes.NewBuffer()"}, wantMessage: "invoking", wantFields: map[string]any{ "function": "bytes.NewBuffer()", "module": "myModule", }, }, { name: "Invoked/Error", give: &Invoked{FunctionName: "bytes.NewBuffer()", Err: someError}, wantMessage: "invoke failed", wantFields: map[string]any{ "error": "some error", "stack": "", "function": "bytes.NewBuffer()", }, }, { name: "Start/Error", give: &Started{Err: someError}, wantMessage: "start failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "Stopping", give: &Stopping{Signal: os.Interrupt}, wantMessage: "received signal", wantFields: map[string]any{ "signal": "INTERRUPT", }, }, { name: "Stopped/Error", give: &Stopped{Err: someError}, wantMessage: "stop failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "RollingBack/Error", give: &RollingBack{StartErr: someError}, wantMessage: "start failed, rolling back", wantFields: map[string]any{ "error": "some error", }, }, { name: "RolledBack/Error", give: &RolledBack{Err: someError}, wantMessage: "rollback failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "Started", give: &Started{}, wantMessage: "started", wantFields: map[string]any{}, }, { name: "LoggerInitialized/Error", give: &LoggerInitialized{Err: someError}, wantMessage: "custom logger initialization failed", wantFields: map[string]any{ "error": "some error", }, }, { name: "LoggerInitialized", give: &LoggerInitialized{ConstructorName: "bytes.NewBuffer()"}, wantMessage: "initialized custom fxevent.Logger", wantFields: map[string]any{ "function": "bytes.NewBuffer()", }, }, } t.Run("debug observer, log at default (info)", func(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() core, observedLogs := observer.New(zap.DebugLevel) (&ZapLogger{Logger: zap.New(core)}).LogEvent(tt.give) logs := observedLogs.TakeAll() require.Len(t, logs, 1) got := logs[0] assert.Equal(t, tt.wantMessage, got.Message) assert.Equal(t, tt.wantFields, got.ContextMap()) }) } }) t.Run("info observer, log at debug", func(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() core, observedLogs := observer.New(zap.InfoLevel) l := &ZapLogger{Logger: zap.New(core)} l.UseLogLevel(zapcore.DebugLevel) l.LogEvent(tt.give) logs := observedLogs.TakeAll() // logs are not visible unless they are errors if strings.HasSuffix(tt.name, "/Error") { require.Len(t, logs, 1) got := logs[0] assert.Equal(t, tt.wantMessage, got.Message) assert.Equal(t, tt.wantFields, got.ContextMap()) } else { require.Len(t, logs, 0) } }) } }) t.Run("info observer, log/error at debug", func(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() core, observedLogs := observer.New(zap.InfoLevel) l := &ZapLogger{Logger: zap.New(core)} l.UseLogLevel(zapcore.DebugLevel) l.UseErrorLevel(zapcore.DebugLevel) l.LogEvent(tt.give) logs := observedLogs.TakeAll() require.Len(t, logs, 0, "no logs should be visible") }) } }) t.Run("test setting log levels", func(t *testing.T) { levels := []zapcore.Level{ zapcore.DebugLevel, zapcore.WarnLevel, zapcore.DPanicLevel, zapcore.PanicLevel, } for _, level := range levels { core, observedLogs := observer.New(level) logger := &ZapLogger{Logger: zap.New(core)} logger.UseLogLevel(level) func() { defer func() { recover() }() logger.LogEvent(&OnStartExecuting{ FunctionName: "hook.onStart", CallerName: "bytes.NewBuffer", }) }() logs := observedLogs.TakeAll() require.Len(t, logs, 1) } }) t.Run("test setting error log levels", func(t *testing.T) { levels := []zapcore.Level{ zapcore.DebugLevel, zapcore.WarnLevel, zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel, } for _, level := range levels { core, observedLogs := observer.New(level) logger := &ZapLogger{Logger: zap.New(core, zap.WithFatalHook(zapcore.WriteThenPanic))} logger.UseErrorLevel(level) func() { defer func() { recover() }() logger.LogEvent(&OnStopExecuted{ FunctionName: "hook.onStart1", CallerName: "bytes.NewBuffer", Err: fmt.Errorf("some error"), }) }() logs := observedLogs.TakeAll() require.Len(t, logs, 1) } }) } ================================================ FILE: fxtest/app.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fxtest import ( "context" "go.uber.org/fx" ) // App is a wrapper around fx.App that provides some testing helpers. By // default, it uses the provided TB as the application's logging backend. type App struct { *fx.App tb TB } // New creates a new test application. func New(tb TB, opts ...fx.Option) *App { allOpts := make([]fx.Option, 0, len(opts)+1) allOpts = append(allOpts, WithTestLogger(tb)) allOpts = append(allOpts, opts...) app := fx.New(allOpts...) if err := app.Err(); err != nil { tb.Errorf("fx.New failed: %v", err) tb.FailNow() } return &App{ App: app, tb: tb, } } // RequireStart calls Start, failing the test if an error is encountered. func (app *App) RequireStart() *App { startCtx, cancel := context.WithTimeout(context.Background(), app.StartTimeout()) defer cancel() if err := app.Start(startCtx); err != nil { app.tb.Errorf("application didn't start cleanly: %v", err) app.tb.FailNow() } return app } // RequireStop calls Stop, failing the test if an error is encountered. func (app *App) RequireStop() { stopCtx, cancel := context.WithTimeout(context.Background(), app.StopTimeout()) defer cancel() if err := app.Stop(stopCtx); err != nil { app.tb.Errorf("application didn't stop cleanly: %v", err) app.tb.FailNow() } } ================================================ FILE: fxtest/app_test.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fxtest import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx" ) func TestApp(t *testing.T) { t.Parallel() t.Run("Success", func(t *testing.T) { t.Parallel() spy := newTB() New(spy).RequireStart().RequireStop() assert.Zero(t, spy.failures, "App didn't start and stop cleanly.") assert.Contains(t, spy.logs.String(), "[Fx] RUNNING", "Expected to write logs to TB.") }) t.Run("NewFailure", func(t *testing.T) { t.Parallel() spy := newTB() New( spy, // Missing string dependency in container. fx.Invoke(func(string) {}), ) assert.Equal(t, 1, spy.failures, "Expected app to error on New.") assert.Contains(t, spy.errors.String(), "New failed", "Expected to write error to TB") }) t.Run("StartError", func(t *testing.T) { t.Parallel() spy := newTB() New( spy, fx.Invoke(func(lc fx.Lifecycle) { lc.Append(fx.Hook{ OnStart: func(context.Context) error { return errors.New("fail") }, }) }), ).RequireStart() assert.Equal(t, 1, spy.failures, "Expected app to error on start.") assert.Contains(t, spy.errors.String(), "didn't start cleanly", "Expected to write errors to TB.") }) t.Run("StopFailure", func(t *testing.T) { t.Parallel() spy := newTB() construct := func(lc fx.Lifecycle) struct{} { lc.Append(fx.Hook{OnStop: func(context.Context) error { return errors.New("fail") }}) return struct{}{} } New( spy, fx.Provide(construct), fx.Invoke(func(struct{}) {}), ).RequireStart().RequireStop() assert.Equal(t, 1, spy.failures, "Expected Stop to fail.") assert.Contains(t, spy.errors.String(), "didn't stop cleanly", "Expected to write errors to TB.") }) } ================================================ FILE: fxtest/doc.go ================================================ // Copyright (c) 2024 Uber Technologies, Inc. // // 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. // Package fxtest provides utilities for testing Fx modules, // and code that directly uses Fx. package fxtest ================================================ FILE: fxtest/lifecycle.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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. package fxtest import ( "context" "fmt" "io" "os" "go.uber.org/fx" "go.uber.org/fx/internal/fxclock" "go.uber.org/fx/internal/fxlog" "go.uber.org/fx/internal/lifecycle" "go.uber.org/fx/internal/testutil" ) // If a testing.T is unspecified, degrade to printing to stderr to provide // meaningful messages. type panicT struct { W io.Writer // stream to which we'll write messages // lastError message written to the stream with Errorf. We'll use this // as the panic message if FailNow is called. lastErr string } var _ TB = &panicT{} func (t *panicT) format(s string, args ...any) string { return fmt.Sprintf(s, args...) } func (t *panicT) Logf(s string, args ...any) { fmt.Fprintln(t.W, t.format(s, args...)) } func (t *panicT) Errorf(s string, args ...any) { t.lastErr = t.format(s, args...) fmt.Fprintln(t.W, t.lastErr) } func (t *panicT) FailNow() { if len(t.lastErr) > 0 { panic(t.lastErr) } panic("test lifecycle failed") } // LifecycleOption modifies the behavior of the [Lifecycle] // when passed to [NewLifecycle]. type LifecycleOption interface { apply(*Lifecycle) } // EnforceTimeout will cause the [Lifecycle]'s Start and Stop methods // to return an error as soon as context expires, // regardless of whether specific hooks respect the timeout. func EnforceTimeout(enforce bool) LifecycleOption { return &enforceTimeout{ enforce: enforce, } } type enforceTimeout struct { enforce bool } func (e *enforceTimeout) apply(lc *Lifecycle) { lc.enforceTimeout = e.enforce } var _ LifecycleOption = (*enforceTimeout)(nil) // Lifecycle is a testing spy for fx.Lifecycle. It exposes Start and Stop // methods (and some test-specific helpers) so that unit tests can exercise // hooks. type Lifecycle struct { t TB lc *lifecycle.Lifecycle enforceTimeout bool } var _ fx.Lifecycle = (*Lifecycle)(nil) // NewLifecycle creates a new test lifecycle. func NewLifecycle(t TB, opts ...LifecycleOption) *Lifecycle { var w io.Writer if t != nil { w = testutil.WriteSyncer{T: t} } else { w = os.Stderr t = &panicT{W: os.Stderr} } lc := &Lifecycle{ lc: lifecycle.New(fxlog.DefaultLogger(w), fxclock.System), t: t, } for _, opt := range opts { opt.apply(lc) } return lc } func (l *Lifecycle) withTimeout(ctx context.Context, fn func(context.Context) error) error { if !l.enforceTimeout { return fn(ctx) } // Cancel on timeout in case function only respects // cancellation and not deadline exceeded. ctx, cancel := context.WithCancel(ctx) defer cancel() c := make(chan error, 1) // buffered to avoid goroutine leak go func() { c <- fn(ctx) }() var err error select { case err = <-c: case <-ctx.Done(): err = ctx.Err() } return err } // Start executes all registered OnStart hooks in order, halting at the first // hook that doesn't succeed. func (l *Lifecycle) Start(ctx context.Context) error { return l.withTimeout(ctx, l.lc.Start) } // RequireStart calls Start with context.Background(), failing the test if an // error is encountered. func (l *Lifecycle) RequireStart() *Lifecycle { ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := l.Start(ctx); err != nil { l.t.Errorf("lifecycle didn't start cleanly: %v", err) l.t.FailNow() } return l } // Stop calls all OnStop hooks whose OnStart counterpart was called, running // in reverse order. // // If any hook returns an error, execution continues for a best-effort // cleanup. Any errors encountered are collected into a single error and // returned. func (l *Lifecycle) Stop(ctx context.Context) error { return l.withTimeout(ctx, l.lc.Stop) } // RequireStop calls Stop with context.Background(), failing the test if an error // is encountered. func (l *Lifecycle) RequireStop() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := l.Stop(ctx); err != nil { l.t.Errorf("lifecycle didn't stop cleanly: %v", err) l.t.FailNow() } } // Append registers a new Hook. func (l *Lifecycle) Append(h fx.Hook) { l.lc.Append(lifecycle.Hook{ OnStart: h.OnStart, OnStop: h.OnStop, }) } ================================================ FILE: fxtest/lifecycle_test.go ================================================ // Copyright (c) 2020 Uber Technologies, Inc. // // 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. package fxtest import ( "bytes" "context" "errors" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "go.uber.org/goleak" ) func TestLifecycle(t *testing.T) { t.Parallel() t.Run("Success", func(t *testing.T) { t.Parallel() spy := newTB() n := 0 lc := NewLifecycle(spy) lc.Append(fx.Hook{ OnStart: func(context.Context) error { n++; return nil }, OnStop: func(context.Context) error { n++; return nil }, }) lc.RequireStart().RequireStop() assert.Zero(t, spy.failures, "Lifecycle start/stop failed.") assert.Equal(t, 2, n, "Didn't run start and stop hooks.") }) t.Run("StartError", func(t *testing.T) { t.Parallel() spy := newTB() lc := NewLifecycle(spy) lc.Append(fx.Hook{OnStart: func(context.Context) error { return errors.New("fail") }}) lc.RequireStart() assert.Equal(t, 1, spy.failures, "Expected lifecycle start to fail.") lc.RequireStop() assert.Equal(t, 1, spy.failures, "Expected lifecycle stop to succeed.") }) t.Run("StopFailure", func(t *testing.T) { t.Parallel() spy := newTB() lc := NewLifecycle(spy) lc.Append(fx.Hook{OnStop: func(context.Context) error { return errors.New("fail") }}) lc.RequireStart() assert.Equal(t, 0, spy.failures, "Expected lifecycle start to succeed.") lc.RequireStop() assert.Equal(t, 1, spy.failures, "Expected lifecycle stop to fail.") }) t.Run("RequireLeakDetection", func(t *testing.T) { t.Parallel() spy := newTB() lc := NewLifecycle(spy) stop := make(chan struct{}) stopped := make(chan struct{}) onStart := func(ctx context.Context) error { go func() { <-stop close(stopped) }() return ctx.Err() } onStop := func(ctx context.Context) error { close(stop) <-stopped return ctx.Err() } lc.Append(fx.Hook{ OnStart: onStart, OnStop: onStop, }) lc.RequireStart() assert.Equal(t, 0, spy.failures, "Expected lifecycle start to succeed.") lc.RequireStop() assert.Equal(t, 0, spy.failures, "Expected lifecycle to stop.") }) } func TestEnforceTimeout(t *testing.T) { // These tests directly call Start and Stop // rather than RequireStart and RequireStop // because EnforceTimeout does not apply to those. t.Run("StartHookTimeout", func(t *testing.T) { t.Parallel() wait := make(chan struct{}) defer close(wait) // force timeout by blocking OnStart until end of test spy := newTB() lc := NewLifecycle(spy, EnforceTimeout(true)) lc.Append(fx.Hook{ OnStart: func(context.Context) error { <-wait return nil }, }) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() assert.ErrorIs(t, lc.Start(ctx), context.DeadlineExceeded) assert.Zero(t, spy.failures) }) t.Run("StopHookTimeout", func(t *testing.T) { t.Parallel() wait := make(chan struct{}) defer close(wait) // force timeout by blocking OnStop until end of test spy := newTB() lc := NewLifecycle(spy, EnforceTimeout(true)) lc.Append(fx.Hook{ OnStop: func(context.Context) error { <-wait return nil }, }) require.NoError(t, lc.Start(context.Background())) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() assert.ErrorIs(t, lc.Stop(ctx), context.DeadlineExceeded) assert.Zero(t, spy.failures) }) t.Run("NoTimeout", func(t *testing.T) { t.Parallel() var ( started bool stopped bool ) spy := newTB() lc := NewLifecycle(spy, EnforceTimeout(true)) lc.Append(fx.Hook{ OnStart: func(context.Context) error { started = true return nil }, OnStop: func(context.Context) error { stopped = true return nil }, }) ctx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() require.NoError(t, lc.Start(ctx)) require.NoError(t, lc.Stop(ctx)) assert.True(t, started) assert.True(t, stopped) assert.Zero(t, spy.failures) }) t.Run("OtherError", func(t *testing.T) { t.Parallel() spy := newTB() lc := NewLifecycle(spy, EnforceTimeout(true)) lc.Append(fx.Hook{ OnStart: func(context.Context) error { return errors.New("NOT a context-related error") }, }) ctx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() assert.ErrorContains(t, lc.Start(ctx), "NOT a context-related error") assert.Zero(t, spy.failures) }) } func TestLifecycle_OptionalT(t *testing.T) { t.Parallel() t.Run("success", func(t *testing.T) { t.Parallel() lc := NewLifecycle(nil) var started, stopped bool defer func() { assert.True(t, started, "not started") assert.True(t, stopped, "not stopped") }() lc.Append(fx.Hook{ OnStart: func(context.Context) error { assert.False(t, started, "started twice") started = true return nil }, OnStop: func(context.Context) error { assert.True(t, started, "not yet started") assert.False(t, stopped, "stopped twice") stopped = true return nil }, }) lc.RequireStart().RequireStop() }) t.Run("start error", func(t *testing.T) { t.Parallel() lc := NewLifecycle(nil) lc.Append(fx.Hook{ OnStart: func(context.Context) error { return errors.New("great sadness") }, }) var pval any func() { defer func() { pval = recover() }() lc.RequireStart() }() require.NotNil(t, pval, "must panic in case of failure") assert.Contains(t, fmt.Sprint(pval), "great sadness") }) } func TestPanicT(t *testing.T) { t.Parallel() t.Run("Logf", func(t *testing.T) { t.Parallel() var buff bytes.Buffer pt := panicT{W: &buff} pt.Logf("hello: %v", "world") assert.Equal(t, "hello: world\n", buff.String()) }) t.Run("Errorf", func(t *testing.T) { t.Parallel() var buff bytes.Buffer pt := panicT{W: &buff} pt.Errorf("hello: %v", "world") assert.Equal(t, "hello: world\n", buff.String()) // Functionally there's no difference between Logf and Errorf // unless FailNow is called. t.Run("FailNow", func(t *testing.T) { t.Parallel() var pval any func() { defer func() { pval = recover() }() pt.FailNow() }() assert.Equal(t, "hello: world", pval) }) }) // FailNow without calling Errorf will use the fixed message. t.Run("FailNow", func(t *testing.T) { t.Parallel() var buff bytes.Buffer pt := panicT{W: &buff} pt.Logf("hello: %v", "world") var pval any func() { defer func() { pval = recover() }() pt.FailNow() }() assert.Equal(t, "test lifecycle failed", pval) }) } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ================================================ FILE: fxtest/printer.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fxtest import ( "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/fx/internal/fxlog" "go.uber.org/fx/internal/testutil" ) // NewTestLogger returns an fxlog.Logger that logs to the testing TB. func NewTestLogger(t TB) fxevent.Logger { return fxlog.DefaultLogger(testutil.WriteSyncer{T: t}) } // WithTestLogger returns an fx.Option that uses the provided TB // as the destination for Fx's log output. func WithTestLogger(t TB) fx.Option { return fx.WithLogger(func() fxevent.Logger { return NewTestLogger(t) }) } type testPrinter struct { TB } // NewTestPrinter returns a fx.Printer that logs to the testing TB. func NewTestPrinter(t TB) fx.Printer { return &testPrinter{t} } func (p *testPrinter) Printf(format string, args ...any) { p.Logf(format, args...) } ================================================ FILE: fxtest/printer_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fxtest import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewTestPrinter(t *testing.T) { t.Parallel() spy := newTB() p := NewTestPrinter(spy) p.Printf("static") p.Printf("dynamic %v", 1) assert.Equal(t, `static dynamic 1 `, spy.logs.String()) } ================================================ FILE: fxtest/tb.go ================================================ // Copyright (c) 2020 Uber Technologies, Inc. // // 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. package fxtest // TB is a subset of the standard library's testing.TB interface. It's // satisfied by both *testing.T and *testing.B. type TB interface { Logf(string, ...any) Errorf(string, ...any) FailNow() } ================================================ FILE: fxtest/tb_test.go ================================================ // Copyright (c) 2020 Uber Technologies, Inc. // // 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. package fxtest import ( "bytes" "fmt" "testing" ) // Verify that TB always matches testing.T. var _ TB = (*testing.T)(nil) type tb struct { failures int errors *bytes.Buffer logs *bytes.Buffer } func newTB() *tb { return &tb{0, &bytes.Buffer{}, &bytes.Buffer{}} } func (t *tb) FailNow() { t.failures++ } func (t *tb) Errorf(format string, args ...any) { fmt.Fprintf(t.errors, format, args...) t.errors.WriteRune('\n') } func (t *tb) Logf(format string, args ...any) { fmt.Fprintf(t.logs, format, args...) t.logs.WriteRune('\n') } ================================================ FILE: go.mod ================================================ module go.uber.org/fx go 1.24 require ( github.com/stretchr/testify v1.8.1 go.uber.org/dig v1.19.0 go.uber.org/goleak v1.2.0 go.uber.org/multierr v1.10.0 go.uber.org/zap v1.26.0 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ 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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: inout.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx import "go.uber.org/dig" // In can be embedded into a struct to mark it as a parameter struct. // This allows it to make use of advanced dependency injection features. // See package documentation for more information. // // It's recommended that shared modules use a single parameter struct to // provide a forward-compatible API: // adding new optional fields to a struct is backward-compatible, // so modules can evolve as needs change. type In = dig.In // Out is the inverse of In: it marks a struct as a result struct so that // it can be used with advanced dependency injection features. // See package documentation for more information. // // It's recommended that shared modules use a single result struct to // provide a forward-compatible API: // adding new fields to a struct is backward-compatible, // so modules can produce more outputs as they grow. type Out = dig.Out ================================================ FILE: inout_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx_test import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/dig" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestIn(t *testing.T) { t.Parallel() type in struct { fx.In } assert.True(t, dig.IsIn(in{}), "Expected dig.In to work with fx.In") } func TestOut(t *testing.T) { t.Parallel() type out struct { fx.Out } assert.True(t, dig.IsOut(out{}), "expected dig.Out to work with fx.Out") } func TestOptionalTypes(t *testing.T) { t.Parallel() type foo struct{} newFoo := func() *foo { return &foo{} } type bar struct{} newBar := func() *bar { return &bar{} } type in struct { fx.In Foo *foo Bar *bar `optional:"true"` } t.Run("NotProvided", func(t *testing.T) { t.Parallel() ran := false app := fxtest.New(t, fx.Provide(newFoo), fx.Invoke(func(in in) { assert.NotNil(t, in.Foo, "foo was not optional and provided, expected not nil") assert.Nil(t, in.Bar, "bar was optional and not provided, expected nil") ran = true })) app.RequireStart().RequireStop() assert.True(t, ran, "expected invoke to run") }) t.Run("Provided", func(t *testing.T) { t.Parallel() ran := false app := fxtest.New(t, fx.Provide(newFoo, newBar), fx.Invoke(func(in in) { assert.NotNil(t, in.Foo, "foo was not optional and provided, expected not nil") assert.NotNil(t, in.Bar, "bar was optional and provided, expected not nil") ran = true })) app.RequireStart().RequireStop() assert.True(t, ran, "expected invoke to run") }) } func TestNamedTypes(t *testing.T) { t.Parallel() type a struct { name string } // a constructor that returns the type a with name "foo" type fooOut struct { fx.Out A *a `name:"foo"` } newFoo := func() fooOut { return fooOut{ A: &a{name: "foo"}, } } // another constructor that returns the same type a with name "bar" type barOut struct { fx.Out A *a `name:"bar"` } newBar := func() barOut { return barOut{ A: &a{name: "bar"}, } } // invoke with an fx.In that resolves both named types type in struct { fx.In Foo *a `name:"foo"` Bar *a `name:"bar"` } ran := false app := fxtest.New(t, fx.Provide(newFoo, newBar), fx.Invoke(func(in in) { assert.NotNil(t, in.Foo, "expected in.Foo to be injected") assert.Equal(t, "foo", in.Foo.name, "expected to get type 'a' of name 'foo'") assert.NotNil(t, in.Bar, "expected in.Bar to be injected") assert.Equal(t, "bar", in.Bar.name, "expected to get a type 'a' of name 'bar'") ran = true })) app.RequireStart().RequireStop() assert.True(t, ran, "expected invoke to run") } func TestIgnoreUnexported(t *testing.T) { t.Parallel() type A struct{ ID int } type B struct{ ID int } type Params struct { fx.In `ignore-unexported:"true"` A A b B // will be ignored } ran := false run := func(in Params) { defer func() { ran = true }() assert.Equal(t, A{1}, in.A, "A must be set") // We provide a B to the container, but because the "b" field // is unexported, we don't expect it to be set. assert.Equal(t, B{0}, in.b, "b must be unset") } defer func() { assert.True(t, ran, "run was never called") }() fxtest.New(t, fx.Supply(A{1}, B{2}), fx.Invoke(run), ).RequireStart().RequireStop() } ================================================ FILE: internal/e2e/README.md ================================================ This directory holds end-to-end tests for Fx. Each subdirectory holds a complete Fx application and a test for it. This is marked as a separate Go module to prevent this code from being bundled with the Fx library and allow for dependencies that don't leak into Fx. ================================================ FILE: internal/e2e/go.mod ================================================ module go.uber.org/fx/internal/e2e go 1.24 require ( github.com/stretchr/testify v1.8.2 go.uber.org/fx v1.19.2 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace go.uber.org/fx => ../.. ================================================ FILE: internal/e2e/go.sum ================================================ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/e2e/shutdowner_run_exitcode/main.go ================================================ // Copyright (c) 2023 Uber Technologies, Inc. // // 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. package main import ( "go.uber.org/fx" ) func main() { fx.New( fx.Invoke(func(shutdowner fx.Shutdowner) error { shutdowner.Shutdown(fx.ExitCode(20)) return nil }), ).Run() } ================================================ FILE: internal/e2e/shutdowner_run_exitcode/main_test.go ================================================ // Copyright (c) 2023 Uber Technologies, Inc. // // 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. package main import ( "os" "os/exec" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/internal/testutil" ) // Hijacks the test binary so that the test can run main() as a subprocess // instead of trying to compile the program and run it directly. func TestMain(m *testing.M) { // If the test binary is named "app", then we're running as a subprocess. // Otherwise, run the tests. switch filepath.Base(os.Args[0]) { case "app": main() os.Exit(0) default: os.Exit(m.Run()) } } // Verifies that an Fx program running with Run // exits with the exit code passed to Shutdowner. // // Regression test for https://github.com/uber-go/fx/issues/1074. func TestShutdownExitCode(t *testing.T) { exe, err := os.Executable() require.NoError(t, err) out := testutil.WriteSyncer{T: t} // Run the test binary with the name 'app' so that it runs main(). cmd := exec.Command(exe) cmd.Args[0] = "app" cmd.Stdout = &out cmd.Stderr = &out // The program should exit with code 20. err = cmd.Run() require.Error(t, err) var exitErr *exec.ExitError require.ErrorAs(t, err, &exitErr) assert.Equal(t, 20, exitErr.ExitCode()) } ================================================ FILE: internal/e2e/shutdowner_wait_exitcode/main.go ================================================ // Copyright (c) 2023 Uber Technologies, Inc. // // 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. package main import ( "context" "log" "os" "time" "go.uber.org/fx" ) func main() { app := fx.New( fx.Invoke(func(shutdowner fx.Shutdowner) error { shutdowner.Shutdown(fx.ExitCode(20)) return nil }), ) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := app.Start(ctx); err != nil { log.Fatal(err) } sig := <-app.Wait() if err := app.Stop(ctx); err != nil { log.Fatal(err) } os.Exit(sig.ExitCode) } ================================================ FILE: internal/e2e/shutdowner_wait_exitcode/main_test.go ================================================ // Copyright (c) 2023 Uber Technologies, Inc. // // 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. package main import ( "os" "os/exec" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/internal/testutil" ) // Hijacks the test binary so that the test can run main() as a subprocess // instead of trying to compile the program and run it directly. func TestMain(m *testing.M) { // If the test binary is named "app", then we're running as a subprocess. // Otherwise, run the tests. switch filepath.Base(os.Args[0]) { case "app": main() os.Exit(0) default: os.Exit(m.Run()) } } // Verifies that an Fx program running with Run // exits with the exit code passed to Shutdowner. // // Regression test for https://github.com/uber-go/fx/issues/1074. func TestShutdownExitCode(t *testing.T) { exe, err := os.Executable() require.NoError(t, err) out := testutil.WriteSyncer{T: t} // Run the test binary with the name 'app' so that it runs main(). cmd := exec.Command(exe) cmd.Args[0] = "app" cmd.Stdout = &out cmd.Stderr = &out // The program should exit with code 20. err = cmd.Run() require.Error(t, err) var exitErr *exec.ExitError require.ErrorAs(t, err, &exitErr) assert.Equal(t, 20, exitErr.ExitCode()) } ================================================ FILE: internal/fxclock/clock.go ================================================ // Copyright (c) 2024 Uber Technologies, Inc. // // 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. package fxclock import ( "context" "sort" "sync" "time" ) // Clock defines how Fx accesses time. // We keep the interface pretty minimal. type Clock interface { Now() time.Time Since(time.Time) time.Duration Sleep(time.Duration) WithTimeout(context.Context, time.Duration) (context.Context, context.CancelFunc) } // System is the default implementation of Clock based on real time. var System Clock = systemClock{} type systemClock struct{} func (systemClock) Now() time.Time { return time.Now() } func (systemClock) Since(t time.Time) time.Duration { return time.Since(t) } func (systemClock) Sleep(d time.Duration) { time.Sleep(d) } func (systemClock) WithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) { return context.WithTimeout(ctx, d) } // Mock adapted from // https://github.com/uber-go/zap/blob/7db06bc9b095571d3dc3d4eebdfbe4dd9bd20405/internal/ztest/clock.go. // Mock is a fake source of time. // It implements standard time operations, // but allows the user to control the passage of time. // // Use the [Add] method to progress time. type Mock struct { mu sync.RWMutex now time.Time // The MockClock works by maintaining a list of waiters. // Each waiter knows the time at which it should be resolved. // When the clock advances, all waiters that are in range are resolved // in chronological order. waiters []waiter waiterAdded *sync.Cond } var _ Clock = (*Mock)(nil) // NewMock builds a new mock clock // using the current actual time as the initial time. func NewMock() *Mock { m := &Mock{now: time.Now()} m.waiterAdded = sync.NewCond(&m.mu) return m } // Now reports the current time. func (c *Mock) Now() time.Time { c.mu.RLock() defer c.mu.RUnlock() return c.now } // Since reports the time elapsed since t. // This is short for Now().Sub(t). func (c *Mock) Since(t time.Time) time.Duration { return c.Now().Sub(t) } // Sleep pauses the current goroutine for the given duration. // // With the mock clock, this will freeze // until the clock is advanced with [Add] past the deadline. func (c *Mock) Sleep(d time.Duration) { ch := make(chan struct{}) c.runAt(c.Now().Add(d), func() { close(ch) }) <-ch } // WithTimeout returns a new context with a deadline of now + d. // // When the deadline is passed, the returned context's Done channel is closed // and the context's Err method returns context.DeadlineExceeded. // If the cancel function is called before the deadline is passed, // the context's Err method returns context.Canceled. func (c *Mock) WithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) { // Unfortunately, we can't use context.WithCancelCause here. // Per its documentation (and verified by trying it): // // ctx, cancel := context.WithCancelCause(parent) // cancel(myError) // ctx.Err() // returns context.Canceled // context.Cause(ctx) // returns myError // // So it won't do for our purposes. deadline := c.Now().Add(d) inner, cancelInner := context.WithCancel(ctx) dctx := &deadlineCtx{ inner: inner, cancelInner: cancelInner, done: make(chan struct{}), deadline: deadline, } ctx = dctx c.runAt(deadline, func() { dctx.cancel(context.DeadlineExceeded) }) return ctx, func() { dctx.cancel(context.Canceled) } } type deadlineCtx struct { inner context.Context cancelInner func() done chan struct{} deadline time.Time mu sync.Mutex // guards err; the rest is immutable err error } var _ context.Context = (*deadlineCtx)(nil) func (c *deadlineCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func (c *deadlineCtx) Done() <-chan struct{} { return c.done } func (c *deadlineCtx) Value(key any) any { return c.inner.Value(key) } func (c *deadlineCtx) Err() error { c.mu.Lock() defer c.mu.Unlock() return c.err } func (c *deadlineCtx) cancel(err error) { c.mu.Lock() if c.err == nil { c.err = err close(c.done) c.cancelInner() } c.mu.Unlock() } // runAt schedules the given function to be run at the given time. // The function runs without a lock held, so it may schedule more work. func (c *Mock) runAt(t time.Time, fn func()) { c.mu.Lock() defer c.mu.Unlock() c.waiters = append(c.waiters, waiter{until: t, fn: fn}) c.waiterAdded.Broadcast() } // AwaitScheduled blocks until there are at least N // operations scheduled for the future. func (c *Mock) AwaitScheduled(n int) { c.mu.Lock() defer c.mu.Unlock() // Note: waiterAdded is associated with c.mu, // the same lock we're holding here. // // When we call Wait(), it'll release the lock // and block until signaled by runAt, // at which point it'll reacquire the lock // (waiting until runAt has released it). for len(c.waiters) < n { c.waiterAdded.Wait() } } type waiter struct { until time.Time fn func() } // Add progresses time by the given duration. // Other operations waiting for the time to advance // will be resolved if they are within range. // // Side effects of operations waiting for the time to advance // will take effect on a best-effort basis. // Avoid racing with operations that have side effects. // // Panics if the duration is negative. func (c *Mock) Add(d time.Duration) { if d < 0 { panic("cannot add negative duration") } c.mu.Lock() defer c.mu.Unlock() sort.Slice(c.waiters, func(i, j int) bool { return c.waiters[i].until.Before(c.waiters[j].until) }) newTime := c.now.Add(d) // newTime won't be recorded until the end of this method. // This ensures that any waiters that are resolved // are resolved at the time they were expecting. for len(c.waiters) > 0 { w := c.waiters[0] if w.until.After(newTime) { break } c.waiters[0] = waiter{} // avoid memory leak c.waiters = c.waiters[1:] // The waiter is within range. // Travel to the time of the waiter and resolve it. c.now = w.until // The waiter may schedule more work // so we must release the lock. c.mu.Unlock() w.fn() // Sleeping here is necessary to let the side effects of waiters // take effect before we continue. time.Sleep(1 * time.Millisecond) c.mu.Lock() } c.now = newTime } ================================================ FILE: internal/fxclock/clock_test.go ================================================ // Copyright (c) 2024 Uber Technologies, Inc. // // 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. package fxclock import ( "context" "sync" "testing" "time" "github.com/stretchr/testify/assert" ) func TestSystemClock(t *testing.T) { clock := System testClock(t, System, clock.Sleep) } func TestMockClock(t *testing.T) { clock := NewMock() testClock(t, clock, clock.Add) } func testClock(t *testing.T, clock Clock, advance func(d time.Duration)) { now := clock.Now() assert.False(t, now.IsZero()) t.Run("Since", func(t *testing.T) { advance(1 * time.Millisecond) assert.NotZero(t, clock.Since(now), "time must have advanced") }) t.Run("Sleep", func(t *testing.T) { start := clock.Now() go func() { // For the mock clock, there's a chance that advance will be // too fast and the Sleep will block forever, waiting for // another advance. The mock clock provides // AwaitScheduled to help with this. // // Since that function is not available on the system clock, // we'll use upcasting to check for it. if awaiter, ok := clock.(interface{ AwaitScheduled(int) }); ok { awaiter.AwaitScheduled(1) } advance(1 * time.Millisecond) }() clock.Sleep(1 * time.Millisecond) assert.NotZero(t, clock.Since(start), "time must have advanced") }) t.Run("WithTimeout", func(t *testing.T) { ctx, cancel := clock.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() t.Run("Deadline", func(t *testing.T) { dl, ok := ctx.Deadline() assert.True(t, ok, "must have a deadline") assert.True(t, dl.After(now), "deadline must be in the future") }) advance(1 * time.Millisecond) select { case <-ctx.Done(): assert.Error(t, ctx.Err(), "done context must error") assert.ErrorIs(t, ctx.Err(), context.DeadlineExceeded, "context must have exceeded its deadline") case <-time.After(10 * time.Millisecond): t.Fatal("expected context to be done") } }) t.Run("WithTimeout/Value", func(t *testing.T) { type contextKey string key := contextKey("foo") ctx1 := context.WithValue(context.Background(), key, "bar") ctx2, cancel := clock.WithTimeout(ctx1, 1*time.Millisecond) defer cancel() assert.Equal(t, "bar", ctx2.Value(key), "value must be preserved") }) t.Run("WithTimeout/Cancel", func(t *testing.T) { ctx, cancel := clock.WithTimeout(context.Background(), 1*time.Millisecond) cancel() select { case <-ctx.Done(): assert.Error(t, ctx.Err(), "done context must error") assert.ErrorIs(t, ctx.Err(), context.Canceled, "context must have been canceled") case <-time.After(10 * time.Millisecond): t.Fatal("expected context to be done") } }) } func TestMock_Sleep(t *testing.T) { clock := NewMock() ch := make(chan struct{}) go func() { clock.Sleep(2 * time.Millisecond) close(ch) }() // We cannot advance time until we're certain // that the Sleep call has started waiting. // Otherwise, we'll advance that one millisecond, // and then the Sleep will start waiting for another Advance, // which will never come. // // AwaitScheduled will block until there is at least one // scheduled event. clock.AwaitScheduled(1) // Advance only one millisecond, the Sleep should not return. clock.Add(1 * time.Millisecond) select { case <-ch: t.Fatal("sleep should not have returned") case <-time.After(1 * time.Millisecond): // ok } // Avance to the next millisecond, the Sleep should return. clock.Add(1 * time.Millisecond) select { case <-ch: // ok case <-time.After(10 * time.Millisecond): t.Fatal("expected Sleep to return") } } func TestMock_AddNegative(t *testing.T) { clock := NewMock() assert.Panics(t, func() { clock.Add(-1) }) } func TestMock_ManySleepers(t *testing.T) { const N = 100 clock := NewMock() var wg sync.WaitGroup wg.Add(N) for range N { go func() { defer wg.Done() clock.Sleep(1 * time.Millisecond) }() } clock.AwaitScheduled(N) clock.Add(1 * time.Millisecond) done := make(chan struct{}) go func() { defer close(done) wg.Wait() }() select { case <-done: // ok case <-time.After(10 * time.Millisecond): t.Fatal("expected all sleepers to be done") } } ================================================ FILE: internal/fxlog/default.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fxlog import ( "io" "go.uber.org/fx/fxevent" ) // DefaultLogger constructs a Logger out of io.Writer. func DefaultLogger(w io.Writer) fxevent.Logger { return &fxevent.ConsoleLogger{W: w} } ================================================ FILE: internal/fxlog/default_test.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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. package fxlog import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/internal/testutil" "go.uber.org/goleak" ) func TestNew(t *testing.T) { t.Parallel() assert.NotPanics(t, func() { DefaultLogger(testutil.WriteSyncer{T: t}) }) } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ================================================ FILE: internal/fxlog/foovendor/foovendor.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package foovendor // New is a test case for vendor path shortening func New() string { return "foovendor" } ================================================ FILE: internal/fxlog/sample.git/sample.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package sample // New is a test constructor for a string func New() (string, error) { return "Hi", nil } ================================================ FILE: internal/fxlog/spy.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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. package fxlog import ( "reflect" "sync" "go.uber.org/fx/fxevent" ) // Events is a list of events captured by fxlog.Spy. type Events []fxevent.Event // Len returns the number of events in this list. func (es Events) Len() int { return len(es) } // SelectByTypeName returns a new list with only events matching the specified // type. func (es Events) SelectByTypeName(name string) Events { var out Events for _, e := range es { if reflect.TypeOf(e).Elem().Name() == name { out = append(out, e) } } return out } // Spy is an Fx event logger that captures emitted events and/or logged // statements. It may be used in tests of Fx logs. type Spy struct { mu sync.RWMutex events Events } var _ fxevent.Logger = &Spy{} // LogEvent appends an Event. func (s *Spy) LogEvent(event fxevent.Event) { s.mu.Lock() s.events = append(s.events, event) s.mu.Unlock() } // Events returns all captured events. func (s *Spy) Events() Events { s.mu.RLock() defer s.mu.RUnlock() events := make(Events, len(s.events)) copy(events, s.events) return events } // EventTypes returns all captured event types. func (s *Spy) EventTypes() []string { s.mu.RLock() defer s.mu.RUnlock() types := make([]string, len(s.events)) for i, e := range s.events { types[i] = reflect.TypeOf(e).Elem().Name() } return types } // Reset clears all messages and events from the Spy. func (s *Spy) Reset() { s.mu.Lock() s.events = s.events[:0] s.mu.Unlock() } ================================================ FILE: internal/fxlog/spy_test.go ================================================ // Copyright (c) 2020 Uber Technologies, Inc. // // 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. package fxlog import ( "fmt" "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/fxevent" ) func TestSpy(t *testing.T) { t.Parallel() var s Spy t.Run("empty spy", func(t *testing.T) { assert.Empty(t, s.Events(), "events must be empty") assert.Zero(t, s.Events().Len(), "events length must be zero") assert.Empty(t, s.EventTypes(), "event types must be empty") }) s.LogEvent(&fxevent.Started{}) t.Run("use after reset", func(t *testing.T) { assert.Equal(t, "Started", s.EventTypes()[0]) }) s.LogEvent(&fxevent.Provided{Err: fmt.Errorf("some error")}) t.Run("some error", func(t *testing.T) { assert.Equal(t, 1, s.Events().SelectByTypeName("Provided").Len()) assert.Equal(t, "Provided", s.EventTypes()[1]) }) s.Reset() t.Run("reset", func(t *testing.T) { assert.Empty(t, s.Events(), "events must be empty") assert.Empty(t, s.EventTypes(), "event types must be empty") }) s.LogEvent(&fxevent.Started{}) t.Run("use after reset", func(t *testing.T) { assert.Equal(t, "Started", s.EventTypes()[0]) }) } ================================================ FILE: internal/fxreflect/fxreflect.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fxreflect import ( "fmt" "net/url" "reflect" "regexp" "runtime" "strings" ) // Match from beginning of the line until the first `vendor/` (non-greedy) var vendorRe = regexp.MustCompile("^.*?/vendor/") // sanitize makes the function name suitable for logging display. It removes // url-encoded elements from the `dot.git` package names and shortens the // vendored paths. func sanitize(function string) string { // Use the stdlib to un-escape any package import paths which can happen // in the case of the "dot-git" postfix. Seems like a bug in stdlib =/ if unescaped, err := url.QueryUnescape(function); err == nil { function = unescaped } // strip everything prior to the vendor return vendorRe.ReplaceAllString(function, "vendor/") } // Caller returns the formatted calling func name func Caller() string { return CallerStack(1, 0).CallerName() } // FuncName returns a funcs formatted name func FuncName(fn any) string { fnV := reflect.ValueOf(fn) if fnV.Kind() != reflect.Func { return fmt.Sprint(fn) } function := runtime.FuncForPC(fnV.Pointer()).Name() return fmt.Sprintf("%s()", sanitize(function)) } // Ascend the call stack until we leave the Fx production code. This allows us // to avoid hard-coding a frame skip, which makes this code work well even // when it's wrapped. func shouldIgnoreFrame(f Frame) bool { // Treat test files as leafs. if strings.Contains(f.File, "_test.go") { return false } // The unique, fully-qualified name for all functions begins with // "{{importPath}}.". We'll ignore Fx and its subpackages. s := strings.TrimPrefix(f.Function, "go.uber.org/fx") if len(s) > 0 && s[0] == '.' || s[0] == '/' { // We want to match, // go.uber.org/fx.Foo // go.uber.org/fx/something.Foo // But not, go.uber.org/fxfoo return true } return false } ================================================ FILE: internal/fxreflect/fxreflect_test.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fxreflect import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/goleak" ) func TestCaller(t *testing.T) { t.Parallel() assert.Equal(t, "go.uber.org/fx/internal/fxreflect.TestCaller", Caller()) } func someFunc() {} func TestFuncName(t *testing.T) { t.Parallel() tests := []struct { desc string give any want string }{ { desc: "function", give: someFunc, want: "go.uber.org/fx/internal/fxreflect.someFunc()", }, { desc: "not a function", give: 42, want: "42", }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() assert.Equal(t, tt.want, FuncName(tt.give)) }) } } func TestSanitizeFuncNames(t *testing.T) { t.Parallel() cases := []struct { name string input string expected string }{ { "url encoding", "go.uber.org/fx/sample%2egit/someFunc", "go.uber.org/fx/sample.git/someFunc", }, { "vendor removal", "go.uber.org/fx/vendor/github.com/some/lib.SomeFunc", "vendor/github.com/some/lib.SomeFunc", }, { "package happens to be named vendor is untouched", "go.uber.org/fx/foovendor/someFunc", "go.uber.org/fx/foovendor/someFunc", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { t.Parallel() assert.Equal(t, c.expected, sanitize(c.input)) }) } } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ================================================ FILE: internal/fxreflect/stack.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fxreflect import ( "fmt" "io" "runtime" "strings" ) // Frame holds information about a single frame in the call stack. type Frame struct { // Unique, package path-qualified name for the function of this call // frame. Function string // File and line number of our location in the frame. // // Note that the line number does not refer to where the function was // defined but where in the function the next call was made. File string Line int } func (f Frame) String() string { // This takes the following forms. // (path/to/file.go) // (path/to/file.go:42) // path/to/package.MyFunction // path/to/package.MyFunction (path/to/file.go) // path/to/package.MyFunction (path/to/file.go:42) var sb strings.Builder sb.WriteString(f.Function) if len(f.File) > 0 { if sb.Len() > 0 { sb.WriteRune(' ') } fmt.Fprintf(&sb, "(%v", f.File) if f.Line > 0 { fmt.Fprintf(&sb, ":%d", f.Line) } sb.WriteRune(')') } if sb.Len() == 0 { return "unknown" } return sb.String() } const _defaultCallersDepth = 8 // Stack is a stack of call frames. // // Formatted with %v, the output is in a single-line, in the form, // // foo/bar.Baz() (path/to/foo.go:42); bar/baz.Qux() (bar/baz/qux.go:12); ... // // Formatted with %+v, the output is in the form, // // foo/bar.Baz() // path/to/foo.go:42 // bar/baz.Qux() // bar/baz/qux.go:12 type Stack []Frame // String returns a single-line, semi-colon representation of a Stack. // For a list of strings where each represents one frame, use Strings. // For a cleaner multi-line representation, use %+v. func (fs Stack) String() string { return strings.Join(fs.Strings(), "; ") } // Strings returns a list of strings, each representing a frame in the stack. // Each line will be in the form, // // foo/bar.Baz() (path/to/foo.go:42) func (fs Stack) Strings() []string { items := make([]string, len(fs)) for i, f := range fs { items[i] = f.String() } return items } // Format implements fmt.Formatter to handle "%+v". func (fs Stack) Format(w fmt.State, c rune) { if !w.Flag('+') { // Without %+v, fall back to String(). io.WriteString(w, fs.String()) return } for _, f := range fs { fmt.Fprintln(w, f.Function) fmt.Fprintf(w, "\t%v:%v\n", f.File, f.Line) } } // CallerName returns the name of the first caller in this stack that isn't // owned by the Fx library. func (fs Stack) CallerName() string { for _, f := range fs { if shouldIgnoreFrame(f) { continue } return f.Function } return "n/a" } // CallerStack returns the call stack for the calling function, up to depth frames // deep, skipping the provided number of frames, not including Callers itself. // // If zero, depth defaults to 8. func CallerStack(skip, depth int) Stack { if depth <= 0 { depth = _defaultCallersDepth } pcs := make([]uintptr, depth) // +2 to skip this frame and runtime.Callers. n := runtime.Callers(skip+2, pcs) pcs = pcs[:n] // truncate to number of frames actually read result := make([]Frame, 0, n) frames := runtime.CallersFrames(pcs) for f, more := frames.Next(); more; f, more = frames.Next() { result = append(result, Frame{ Function: sanitize(f.Function), File: f.File, Line: f.Line, }) } return result } ================================================ FILE: internal/fxreflect/stack_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fxreflect import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStack(t *testing.T) { t.Parallel() // NOTE: // We don't assert the length of the stack because we cannot make // guarantees about how many frames the test runner is allowed to // introduce. t.Run("default", func(t *testing.T) { t.Parallel() frames := CallerStack(0, 0) require.NotEmpty(t, frames) f := frames[0] assert.Equal(t, "go.uber.org/fx/internal/fxreflect.TestStack.func1", f.Function) assert.Contains(t, f.File, "internal/fxreflect/stack_test.go") assert.NotZero(t, f.Line) }) t.Run("skip", func(t *testing.T) { t.Parallel() // Introduce a few frames and skip 2. frames := func() []Frame { return func() []Frame { return CallerStack(2, 0) }() }() require.NotEmpty(t, frames) f := frames[0] assert.Equal(t, "go.uber.org/fx/internal/fxreflect.TestStack.func2", f.Function) assert.Contains(t, f.File, "internal/fxreflect/stack_test.go") assert.NotZero(t, f.Line) }) } func TestDeepStack(t *testing.T) { t.Run("nest", func(t *testing.T) { // Introduce a few frames. frames := func() []Frame { return func() []Frame { return CallerStack(0, 0) }() }() require.True(t, len(frames) > 3, "expected at least three frames") for i, name := range []string{"func1.TestDeepStack.func1.1.2", "func1.1", "func1"} { f := frames[i] assert.Equal(t, "go.uber.org/fx/internal/fxreflect.TestDeepStack."+name, f.Function) assert.Contains(t, f.File, "internal/fxreflect/stack_test.go") assert.NotZero(t, f.Line) } }) } func TestStackCallerName(t *testing.T) { t.Parallel() tests := []struct { desc string give Stack want string }{ {desc: "empty", want: "n/a"}, { desc: "skip Fx components", give: Stack{ { Function: "go.uber.org/fx.Foo()", File: "go.uber.org/fx/foo.go", }, { Function: "foo/bar.Baz()", File: "foo/bar/baz.go", }, }, want: "foo/bar.Baz()", }, { desc: "skip Fx in wrong directory", give: Stack{ { Function: "go.uber.org/fx.Foo()", File: "fx/foo.go", }, { Function: "foo/bar.Baz()", File: "foo/bar/baz.go", }, }, want: "foo/bar.Baz()", }, { desc: "skip Fx subpackage", give: Stack{ { Function: "go.uber.org/fx/internal/fxreflect.Foo()", File: "fx/internal/fxreflect/foo.go", }, { Function: "foo/bar.Baz()", File: "foo/bar/baz.go", }, }, want: "foo/bar.Baz()", }, { desc: "don't skip Fx tests", give: Stack{ { Function: "some/thing.Foo()", File: "go.uber.org/fx/foo_test.go", }, }, want: "some/thing.Foo()", }, { desc: "don't skip fx prefix", give: Stack{ { Function: "go.uber.org/fxfoo.Bar()", File: "go.uber.org/fxfoo/bar.go", }, }, want: "go.uber.org/fxfoo.Bar()", }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() assert.Equal(t, tt.want, tt.give.CallerName()) }) } } func TestFrameString(t *testing.T) { t.Parallel() tests := []struct { desc string give Frame want string }{ { desc: "zero", give: Frame{}, want: "unknown", }, { desc: "file and line", give: Frame{File: "foo.go", Line: 42}, want: "(foo.go:42)", }, { desc: "file only", give: Frame{File: "foo.go"}, want: "(foo.go)", }, { desc: "function only", give: Frame{Function: "foo"}, want: "foo", }, { desc: "function and file", give: Frame{Function: "foo", File: "bar.go"}, want: "foo (bar.go)", }, { desc: "function and line", give: Frame{Function: "foo", Line: 42}, want: "foo", // line without file is meaningless }, { desc: "function, file, and line", give: Frame{Function: "foo", File: "bar.go", Line: 42}, want: "foo (bar.go:42)", }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() assert.Equal(t, tt.want, tt.give.String()) }) } } func TestStackFormat(t *testing.T) { t.Parallel() stack := Stack{ { Function: "path/to/module.SomeFunction()", File: "path/to/file.go", Line: 42, }, { Function: "path/to/another/module.AnotherFunction()", File: "path/to/another/file.go", Line: 12, }, } t.Run("single line", func(t *testing.T) { t.Parallel() assert.Equal(t, "path/to/module.SomeFunction() (path/to/file.go:42); "+ "path/to/another/module.AnotherFunction() (path/to/another/file.go:12)", fmt.Sprintf("%v", stack)) }) t.Run("multi line", func(t *testing.T) { t.Parallel() assert.Equal(t, `path/to/module.SomeFunction() path/to/file.go:42 path/to/another/module.AnotherFunction() path/to/another/file.go:12 `, fmt.Sprintf("%+v", stack)) }) t.Run("strings", func(t *testing.T) { t.Parallel() assert.Equal(t, []string{ "path/to/module.SomeFunction() (path/to/file.go:42)", "path/to/another/module.AnotherFunction() (path/to/another/file.go:12)", }, stack.Strings(), ) }) } ================================================ FILE: internal/leaky_test/leaky_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. // leaky_test contains tests that are explicitly leaking goroutines because they // trigger a panic on purpose. // To prevent these from making it difficult for us to use goleak for tests in // fx_test, we contain these here. package leaky_test import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" ) func TestRecoverFromPanicsOption(t *testing.T) { t.Parallel() t.Run("CannotGiveToModule", func(t *testing.T) { t.Parallel() mod := fx.Module("MyModule", fx.RecoverFromPanics()) err := fx.New(mod).Err() require.Error(t, err) require.Contains(t, err.Error(), "fx.RecoverFromPanics Option should be passed to top-level App, "+ "not to fx.Module") }) run := func(withOption bool) { opts := []fx.Option{ fx.Provide(func() int { panic("terrible sorrow") }), fx.Invoke(func(a int) {}), } if withOption { opts = append(opts, fx.RecoverFromPanics()) app := fx.New(opts...) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), `panic: "terrible sorrow" in func: "go.uber.org/fx/internal/leaky_test_test".TestRecoverFromPanicsOption.`) } else { assert.Panics(t, func() { fx.New(opts...) }, "expected panic without RecoverFromPanics() option") } } t.Run("WithoutOption", func(t *testing.T) { run(false) }) t.Run("WithOption", func(t *testing.T) { run(true) }) } ================================================ FILE: internal/lifecycle/lifecycle.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package lifecycle import ( "context" "errors" "fmt" "io" "reflect" "strings" "sync" "time" "go.uber.org/fx/fxevent" "go.uber.org/fx/internal/fxclock" "go.uber.org/fx/internal/fxreflect" "go.uber.org/multierr" ) // Reflection types for each of the supported hook function signatures. These // are used in cases in which the Callable constraint matches a user-defined // function type that cannot be converted to an underlying function type with // a conventional conversion or type switch. var ( _reflFunc = reflect.TypeOf(Func(nil)) _reflErrorFunc = reflect.TypeOf(ErrorFunc(nil)) _reflContextFunc = reflect.TypeOf(ContextFunc(nil)) _reflContextErrorFunc = reflect.TypeOf(ContextErrorFunc(nil)) ) // Discrete function signatures that are allowed as part of a [Callable]. type ( // A Func can be converted to a ContextErrorFunc. Func = func() // An ErrorFunc can be converted to a ContextErrorFunc. ErrorFunc = func() error // A ContextFunc can be converted to a ContextErrorFunc. ContextFunc = func(context.Context) // A ContextErrorFunc is used as a [Hook.OnStart] or [Hook.OnStop] // function. ContextErrorFunc = func(context.Context) error ) // A Callable is a constraint that matches functions that are, or can be // converted to, functions suitable for a Hook. // // Callable must be identical to [fx.HookFunc]. type Callable interface { ~Func | ~ErrorFunc | ~ContextFunc | ~ContextErrorFunc } // Wrap wraps x into a ContextErrorFunc suitable for a Hook. func Wrap[T Callable](x T) (ContextErrorFunc, string) { if x == nil { return nil, "" } switch fn := any(x).(type) { case Func: return func(context.Context) error { fn() return nil }, fxreflect.FuncName(x) case ErrorFunc: return func(context.Context) error { return fn() }, fxreflect.FuncName(x) case ContextFunc: return func(ctx context.Context) error { fn(ctx) return nil }, fxreflect.FuncName(x) case ContextErrorFunc: return fn, fxreflect.FuncName(x) } // Since (1) we're already using reflect in Fx, (2) we're not particularly // concerned with performance, and (3) unsafe would require discrete build // targets for appengine (etc), just use reflect to convert user-defined // function types to their underlying function types and then call Wrap // again with the converted value. reflVal := reflect.ValueOf(x) switch { case reflVal.CanConvert(_reflFunc): return Wrap(reflVal.Convert(_reflFunc).Interface().(Func)) case reflVal.CanConvert(_reflErrorFunc): return Wrap(reflVal.Convert(_reflErrorFunc).Interface().(ErrorFunc)) case reflVal.CanConvert(_reflContextFunc): return Wrap(reflVal.Convert(_reflContextFunc).Interface().(ContextFunc)) default: // Is already convertible to ContextErrorFunc. return Wrap(reflVal.Convert(_reflContextErrorFunc).Interface().(ContextErrorFunc)) } } // A Hook is a pair of start and stop callbacks, either of which can be nil, // plus a string identifying the supplier of the hook. type Hook struct { OnStart func(context.Context) error OnStop func(context.Context) error OnStartName string OnStopName string callerFrame fxreflect.Frame } type appState int const ( stopped appState = iota starting incompleteStart started stopping ) func (as appState) String() string { switch as { case stopped: return "stopped" case starting: return "starting" case incompleteStart: return "incompleteStart" case started: return "started" case stopping: return "stopping" default: return "invalidState" } } // Lifecycle coordinates application lifecycle hooks. type Lifecycle struct { clock fxclock.Clock logger fxevent.Logger state appState hooks []Hook numStarted int startRecords HookRecords stopRecords HookRecords runningHook Hook mu sync.Mutex } // New constructs a new Lifecycle. func New(logger fxevent.Logger, clock fxclock.Clock) *Lifecycle { return &Lifecycle{logger: logger, clock: clock} } // Append adds a Hook to the lifecycle. func (l *Lifecycle) Append(hook Hook) { // Save the caller's stack frame to report file/line number. if f := fxreflect.CallerStack(2, 0); len(f) > 0 { hook.callerFrame = f[0] } l.hooks = append(l.hooks, hook) } // Start runs all OnStart hooks, returning immediately if it encounters an // error. func (l *Lifecycle) Start(ctx context.Context) error { if ctx == nil { return errors.New("called OnStart with nil context") } l.mu.Lock() if l.state != stopped { defer l.mu.Unlock() return fmt.Errorf("attempted to start lifecycle when in state: %v", l.state) } l.numStarted = 0 l.state = starting l.startRecords = make(HookRecords, 0, len(l.hooks)) l.mu.Unlock() returnState := incompleteStart defer func() { l.mu.Lock() l.state = returnState l.mu.Unlock() }() for _, hook := range l.hooks { // if ctx has cancelled, bail out of the loop. if err := ctx.Err(); err != nil { return err } if hook.OnStart != nil { l.mu.Lock() l.runningHook = hook l.mu.Unlock() runtime, err := l.runStartHook(ctx, hook) if err != nil { return err } l.mu.Lock() l.startRecords = append(l.startRecords, HookRecord{ CallerFrame: hook.callerFrame, Func: hook.OnStart, Runtime: runtime, }) l.mu.Unlock() } l.numStarted++ } returnState = started return nil } func (l *Lifecycle) runStartHook(ctx context.Context, hook Hook) (runtime time.Duration, err error) { funcName := hook.OnStartName if len(funcName) == 0 { funcName = fxreflect.FuncName(hook.OnStart) } l.logger.LogEvent(&fxevent.OnStartExecuting{ CallerName: hook.callerFrame.Function, FunctionName: funcName, }) defer func() { l.logger.LogEvent(&fxevent.OnStartExecuted{ CallerName: hook.callerFrame.Function, FunctionName: funcName, Runtime: runtime, Err: err, }) }() begin := l.clock.Now() err = hook.OnStart(ctx) return l.clock.Since(begin), err } // Stop runs any OnStop hooks whose OnStart counterpart succeeded. OnStop // hooks run in reverse order. func (l *Lifecycle) Stop(ctx context.Context) error { if ctx == nil { return errors.New("called OnStop with nil context") } l.mu.Lock() if l.state != started && l.state != incompleteStart && l.state != starting { defer l.mu.Unlock() return nil } l.state = stopping l.mu.Unlock() defer func() { l.mu.Lock() l.state = stopped l.mu.Unlock() }() l.mu.Lock() l.stopRecords = make(HookRecords, 0, l.numStarted) // Take a snapshot of hook state to avoid races. allHooks := l.hooks[:] numStarted := l.numStarted l.mu.Unlock() // Run backward from last successful OnStart. var errs []error for ; numStarted > 0; numStarted-- { if err := ctx.Err(); err != nil { return err } hook := allHooks[numStarted-1] if hook.OnStop == nil { continue } l.mu.Lock() l.runningHook = hook l.mu.Unlock() runtime, err := l.runStopHook(ctx, hook) if err != nil { // For best-effort cleanup, keep going after errors. errs = append(errs, err) } l.mu.Lock() l.stopRecords = append(l.stopRecords, HookRecord{ CallerFrame: hook.callerFrame, Func: hook.OnStop, Runtime: runtime, }) l.mu.Unlock() } return multierr.Combine(errs...) } func (l *Lifecycle) runStopHook(ctx context.Context, hook Hook) (runtime time.Duration, err error) { funcName := hook.OnStopName if len(funcName) == 0 { funcName = fxreflect.FuncName(hook.OnStop) } l.logger.LogEvent(&fxevent.OnStopExecuting{ CallerName: hook.callerFrame.Function, FunctionName: funcName, }) defer func() { l.logger.LogEvent(&fxevent.OnStopExecuted{ CallerName: hook.callerFrame.Function, FunctionName: funcName, Runtime: runtime, Err: err, }) }() begin := l.clock.Now() err = hook.OnStop(ctx) return l.clock.Since(begin), err } // RunningHookCaller returns the name of the hook that was running when a Start/Stop // hook timed out. func (l *Lifecycle) RunningHookCaller() string { l.mu.Lock() defer l.mu.Unlock() return l.runningHook.callerFrame.Function } // HookRecord keeps track of each Hook's execution time, the caller that appended the Hook, and function that ran as the Hook. type HookRecord struct { CallerFrame fxreflect.Frame // stack frame of the caller Func func(context.Context) error // function that ran as sanitized name Runtime time.Duration // how long the hook ran } // HookRecords is a Stringer wrapper of HookRecord slice. type HookRecords []HookRecord func (rs HookRecords) Len() int { return len(rs) } func (rs HookRecords) Less(i, j int) bool { // Sort by runtime, greater ones at top. return rs[i].Runtime > rs[j].Runtime } func (rs HookRecords) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } // Used for logging startup errors. func (rs HookRecords) String() string { var b strings.Builder for _, r := range rs { fmt.Fprintf(&b, "%s took %v from %s", fxreflect.FuncName(r.Func), r.Runtime, r.CallerFrame) } return b.String() } // Format implements fmt.Formatter to handle "%+v". func (rs HookRecords) Format(w fmt.State, c rune) { if !w.Flag('+') { // Without %+v, fall back to String(). io.WriteString(w, rs.String()) return } for _, r := range rs { fmt.Fprintf(w, "\n%s took %v from:\n\t%+v", fxreflect.FuncName(r.Func), r.Runtime, r.CallerFrame) } fmt.Fprintf(w, "\n") } ================================================ FILE: internal/lifecycle/lifecycle_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package lifecycle import ( "context" "errors" "fmt" "sort" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx/fxevent" "go.uber.org/fx/internal/fxclock" "go.uber.org/fx/internal/fxlog" "go.uber.org/fx/internal/fxreflect" "go.uber.org/fx/internal/testutil" "go.uber.org/goleak" "go.uber.org/multierr" ) func testLogger(t *testing.T) fxevent.Logger { return fxlog.DefaultLogger(testutil.WriteSyncer{T: t}) } func TestLifecycleStart(t *testing.T) { t.Parallel() t.Run("ExecutesInOrder", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) count := 0 l.Append(Hook{ OnStart: func(context.Context) error { count++ assert.Equal(t, 1, count, "expected this starter to be executed first") return nil }, }) l.Append(Hook{ OnStart: func(context.Context) error { count++ assert.Equal(t, 2, count, "expected this starter to be executed second") return nil }, }) assert.NoError(t, l.Start(context.Background())) assert.Equal(t, 2, count) }) t.Run("ErrHaltsChainAndRollsBack", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) err := errors.New("a starter error") starterCount := 0 stopperCount := 0 // this event's starter succeeded, so no matter what the stopper should run l.Append(Hook{ OnStart: func(context.Context) error { starterCount++ return nil }, OnStop: func(context.Context) error { stopperCount++ return nil }, }) // this event's starter fails, so the stopper shouldnt run l.Append(Hook{ OnStart: func(context.Context) error { starterCount++ return err }, OnStop: func(context.Context) error { t.Error("this stopper shouldnt run, since the starter in this event failed") return nil }, }) // this event is last in the chain, so it should never run since the previous failed l.Append(Hook{ OnStart: func(context.Context) error { t.Error("this starter should never run, since the previous event failed") return nil }, OnStop: func(context.Context) error { t.Error("this stopper should never run, since the previous event failed") return nil }, }) assert.Error(t, err, l.Start(context.Background())) assert.NoError(t, l.Stop(context.Background())) assert.Equal(t, 2, starterCount, "expected the first and second starter to execute") assert.Equal(t, 1, stopperCount, "expected the first stopper to execute since the second starter failed") }) t.Run("DoNotRunStartHooksWithExpiredCtx", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) l.Append(Hook{ OnStart: func(context.Context) error { assert.Fail(t, "this hook should not run") return nil }, OnStop: func(context.Context) error { assert.Fail(t, "this hook should not run") return nil }, }) ctx, cancel := context.WithCancel(context.Background()) cancel() err := l.Start(ctx) require.Error(t, err) // Note: Stop does not return an error here because no hooks // have been started, so we don't end up any of the corresponding // stop hooks. require.NoError(t, l.Stop(ctx)) }) t.Run("StartWhileStartedErrors", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) assert.NoError(t, l.Start(context.Background())) err := l.Start(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "attempted to start lifecycle when in state: started") assert.NoError(t, l.Stop(context.Background())) assert.NoError(t, l.Start(context.Background())) }) } func TestLifecycleStop(t *testing.T) { t.Parallel() t.Run("DoesNothingWithoutHooks", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) l.Start(context.Background()) assert.Nil(t, l.Stop(context.Background()), "no lifecycle hooks should have resulted in stop returning nil") }) t.Run("DoesNothingWhenNotStarted", func(t *testing.T) { t.Parallel() hook := Hook{ OnStop: func(context.Context) error { assert.Fail(t, "OnStop should not be called if lifecycle was never started") return nil }, } l := New(testLogger(t), fxclock.System) l.Append(hook) l.Stop(context.Background()) }) t.Run("ExecutesInReverseOrder", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) count := 2 l.Append(Hook{ OnStop: func(context.Context) error { count-- assert.Equal(t, 0, count, "this stopper was added first, so should execute last") return nil }, }) l.Append(Hook{ OnStop: func(context.Context) error { count-- assert.Equal(t, 1, count, "this stopper was added last, so should execute first") return nil }, }) assert.NoError(t, l.Start(context.Background())) assert.NoError(t, l.Stop(context.Background())) assert.Equal(t, 0, count) }) t.Run("ErrDoesntHaltChain", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) count := 0 l.Append(Hook{ OnStop: func(context.Context) error { count++ return nil }, }) err := errors.New("some stop error") l.Append(Hook{ OnStop: func(context.Context) error { count++ return err }, }) assert.NoError(t, l.Start(context.Background())) assert.Equal(t, err, l.Stop(context.Background())) assert.Equal(t, 2, count) }) t.Run("GathersAllErrs", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) err := errors.New("some stop error") err2 := errors.New("some other stop error") l.Append(Hook{ OnStop: func(context.Context) error { return err2 }, }) l.Append(Hook{ OnStop: func(context.Context) error { return err }, }) assert.NoError(t, l.Start(context.Background())) assert.Equal(t, multierr.Combine(err, err2), l.Stop(context.Background())) }) t.Run("AllowEmptyHooks", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) l.Append(Hook{}) l.Append(Hook{}) assert.NoError(t, l.Start(context.Background())) assert.NoError(t, l.Stop(context.Background())) }) t.Run("DoesNothingIfStartFailed", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) err := errors.New("some start error") l.Append(Hook{ OnStart: func(context.Context) error { return err }, OnStop: func(context.Context) error { assert.Fail(t, "OnStop should not be called if start failed") return nil }, }) assert.Equal(t, err, l.Start(context.Background())) l.Stop(context.Background()) }) t.Run("DoNotRunStopHooksWithExpiredCtx", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) l.Append(Hook{ OnStart: func(context.Context) error { return nil }, OnStop: func(context.Context) error { assert.Fail(t, "this hook should not run") return nil }, }) ctx, cancel := context.WithCancel(context.Background()) err := l.Start(ctx) require.NoError(t, err) cancel() require.Error(t, l.Stop(ctx)) }) t.Run("nil ctx", func(t *testing.T) { t.Parallel() l := New(testLogger(t), fxclock.System) l.Append(Hook{ OnStart: func(context.Context) error { assert.Fail(t, "this hook should not run") return nil }, OnStop: func(context.Context) error { assert.Fail(t, "this hook should not run") return nil }, }) err := l.Start(nil) //nolint:staticcheck // SA1012 this test specifically tests for the lint failure require.Error(t, err) assert.Contains(t, err.Error(), "called OnStart with nil context") err = l.Stop(nil) //nolint:staticcheck // SA1012 this test specifically tests for the lint failure require.Error(t, err) assert.Contains(t, err.Error(), "called OnStop with nil context") }) } func TestHookRecordsFormat(t *testing.T) { t.Parallel() t.Run("SortRecords", func(t *testing.T) { t.Parallel() t1, err := time.ParseDuration("10ms") require.NoError(t, err) t2, err := time.ParseDuration("20ms") require.NoError(t, err) f := fxreflect.Frame{ Function: "someFunc", File: "somefunc.go", Line: 1, } r := HookRecords{ HookRecord{ CallerFrame: f, Func: func(context.Context) error { return nil }, Runtime: t1, }, HookRecord{ CallerFrame: f, Func: func(context.Context) error { return nil }, Runtime: t2, }, } sort.Sort(r) for _, format := range []string{"%v", "%+v", "%s"} { s := fmt.Sprintf(format, r) hook1Idx := strings.Index(s, "TestHookRecordsFormat.func1.1()") hook2Idx := strings.Index(s, "TestHookRecordsFormat.func1.2()") assert.Greater(t, hook1Idx, hook2Idx, "second hook must appear first in the formatted string") assert.Contains(t, s, "somefunc.go:1", "file name and line should be reported") } }) } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ================================================ FILE: internal/testutil/writer.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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. package testutil import ( "go.uber.org/zap/zapcore" ) // TestingT is a subset of the testing.T interface. type TestingT interface { Logf(string, ...any) } // WriteSyncer is a zapcore.WriteSyncer that writes to the provided test // logger. type WriteSyncer struct{ T TestingT } var _ zapcore.WriteSyncer = WriteSyncer{} // Write writes the provided bytes to the underlying TestingT. func (w WriteSyncer) Write(bs []byte) (int, error) { w.T.Logf("%s", bs) return len(bs), nil } // Sync is a no-op. func (WriteSyncer) Sync() error { return nil } ================================================ FILE: internal/testutil/writer_test.go ================================================ // Copyright (c) 2020 Uber Technologies, Inc. // // 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. package testutil import ( "fmt" "io" "testing" "github.com/stretchr/testify/assert" ) // spy is a TestingT that captures Log requests. type spy struct{ Logs []string } func (s *spy) Clear() { s.Logs = nil } func (s *spy) Logf(msg string, args ...any) { s.Logs = append(s.Logs, fmt.Sprintf(msg, args...)) } func TestWriteSyncer(t *testing.T) { t.Parallel() var spy spy ws := WriteSyncer{T: &spy} t.Run("log", func(t *testing.T) { t.Parallel() spy.Clear() io.WriteString(ws, "hello") assert.Equal(t, []string{"hello"}, spy.Logs) }) t.Run("sync", func(t *testing.T) { t.Parallel() assert.NoError(t, ws.Sync()) }) } ================================================ FILE: invoke.go ================================================ // Copyright (c) 2019-2021 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "strings" "go.uber.org/fx/internal/fxreflect" ) // Invoke registers functions that are executed eagerly on application start. // Arguments for these invocations are built using the constructors registered // by Provide. Passing multiple Invoke options appends the new invocations to // the application's existing list. // // Unlike constructors, invocations are always executed, and they're always // run in order. Invocations may have any number of returned values. // If the final returned object is an error, it indicates whether the operation // was successful. // All other returned values are discarded. // // Invokes registered in [Module]s are run before the ones registered at the // scope of the parent. Invokes within the same Module is run in the order // they were provided. For example, // // fx.New( // fx.Invoke(func3), // fx.Module("someModule", // fx.Invoke(func1), // fx.Invoke(func2), // ), // fx.Invoke(func4), // ) // // invokes func1, func2, func3, func4 in that order. // // Typically, invoked functions take a handful of high-level objects (whose // constructors depend on lower-level objects) and introduce them to each // other. This kick-starts the application by forcing it to instantiate a // variety of types. // // To see an invocation in use, read through the package-level example. For // advanced features, including optional parameters and named instances, see // the documentation of the In and Out types. func Invoke(funcs ...any) Option { return invokeOption{ Targets: funcs, Stack: fxreflect.CallerStack(1, 0), } } type invokeOption struct { Targets []any Stack fxreflect.Stack } func (o invokeOption) apply(mod *module) { for _, target := range o.Targets { mod.invokes = append(mod.invokes, invoke{ Target: target, Stack: o.Stack, }) } } func (o invokeOption) String() string { items := make([]string, len(o.Targets)) for i, f := range o.Targets { items[i] = fxreflect.FuncName(f) } return fmt.Sprintf("fx.Invoke(%s)", strings.Join(items, ", ")) } func runInvoke(c container, i invoke) error { fn := i.Target switch fn := fn.(type) { case Option: return fmt.Errorf("fx.Option should be passed to fx.New directly, "+ "not to fx.Invoke: fx.Invoke received %v from:\n%+v", fn, i.Stack) case annotated: af, err := fn.Build() if err != nil { return err } return c.Invoke(af) default: return c.Invoke(fn) } } ================================================ FILE: lifecycle.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx import ( "context" "go.uber.org/fx/internal/lifecycle" ) // A HookFunc is a function that can be used as a [Hook]. type HookFunc interface { ~func() | ~func() error | ~func(context.Context) | ~func(context.Context) error } // Lifecycle allows constructors to register callbacks that are executed on // application start and stop. See the documentation for App for details on Fx // applications' initialization, startup, and shutdown logic. type Lifecycle interface { Append(Hook) } // A Hook is a pair of start and stop callbacks, either of which can be nil. // If a Hook's OnStart callback isn't executed (because a previous OnStart // failure short-circuited application startup), its OnStop callback won't be // executed. type Hook struct { OnStart func(context.Context) error OnStop func(context.Context) error onStartName string onStopName string } // StartHook returns a new Hook with start as its [Hook.OnStart] function, // wrapping its signature as needed. For example, given the following function: // // func myfunc() { // fmt.Println("hook called") // } // // then calling: // // lifecycle.Append(StartHook(myfunc)) // // is functionally equivalent to calling: // // lifecycle.Append(fx.Hook{ // OnStart: func(context.Context) error { // myfunc() // return nil // }, // }) // // The same is true for all functions that satisfy the HookFunc constraint. // Note that any context.Context parameter or error return will be propagated // as expected. If propagation is not intended, users should instead provide a // closure that discards the undesired value(s), or construct a Hook directly. func StartHook[T HookFunc](start T) Hook { onstart, startname := lifecycle.Wrap(start) return Hook{ OnStart: onstart, onStartName: startname, } } // StopHook returns a new Hook with stop as its [Hook.OnStop] function, // wrapping its signature as needed. For example, given the following function: // // func myfunc() { // fmt.Println("hook called") // } // // then calling: // // lifecycle.Append(StopHook(myfunc)) // // is functionally equivalent to calling: // // lifecycle.Append(fx.Hook{ // OnStop: func(context.Context) error { // myfunc() // return nil // }, // }) // // The same is true for all functions that satisfy the HookFunc constraint. // Note that any context.Context parameter or error return will be propagated // as expected. If propagation is not intended, users should instead provide a // closure that discards the undesired value(s), or construct a Hook directly. func StopHook[T HookFunc](stop T) Hook { onstop, stopname := lifecycle.Wrap(stop) return Hook{ OnStop: onstop, onStopName: stopname, } } // StartStopHook returns a new Hook with start as its [Hook.OnStart] function // and stop as its [Hook.OnStop] function, independently wrapping the signature // of each as needed. func StartStopHook[T1 HookFunc, T2 HookFunc](start T1, stop T2) Hook { var ( onstart, startname = lifecycle.Wrap(start) onstop, stopname = lifecycle.Wrap(stop) ) return Hook{ OnStart: onstart, OnStop: onstop, onStartName: startname, onStopName: stopname, } } type lifecycleWrapper struct { *lifecycle.Lifecycle } func (l *lifecycleWrapper) Append(h Hook) { l.Lifecycle.Append(lifecycle.Hook{ OnStart: h.OnStart, OnStop: h.OnStop, OnStartName: h.onStartName, OnStopName: h.onStopName, }) } ================================================ FILE: log.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fx import ( "go.uber.org/fx/fxevent" ) // logBuffer will buffer all messages until a logger has been // initialized. type logBuffer struct { events []fxevent.Event logger fxevent.Logger } // LogEvent buffers or logs an event. func (l *logBuffer) LogEvent(event fxevent.Event) { if l.logger == nil { l.events = append(l.events, event) } else { l.logger.LogEvent(event) } } // Connect flushes out all buffered events to a logger and resets them. func (l *logBuffer) Connect(logger fxevent.Logger) { l.logger = logger for _, e := range l.events { logger.LogEvent(e) } l.events = nil } ================================================ FILE: log_test.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package fx import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/fx/fxevent" "go.uber.org/fx/internal/fxlog" "go.uber.org/zap" "go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest/observer" ) func TestLogBufferConnect(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) event := &fxevent.Started{} lb := &logBuffer{ events: []fxevent.Event{event}, logger: nil, } lb.Connect(spy) assert.Equal(t, fxlog.Events{event}, spy.Events()) } func TestLogBufferLog(t *testing.T) { t.Parallel() spy := new(fxlog.Spy) event := &fxevent.Started{} lb := &logBuffer{ events: nil, logger: nil, } lb.LogEvent(event) lb.Connect(spy) assert.Equal(t, fxlog.Events{event}, spy.Events()) } func TestWithLoggerDecorate(t *testing.T) { t.Parallel() core, logs := observer.New(zap.DebugLevel) New( Supply(zaptest.NewLogger(t)), // provide a logger Replace(zap.New(core)), // and replace it WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), ) assert.NotZero(t, logs.Len(), "should post to replacement logger") } ================================================ FILE: module.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "go.uber.org/dig" "go.uber.org/fx/fxevent" "go.uber.org/fx/internal/fxreflect" "go.uber.org/multierr" ) // A container represents a set of constructors to provide // dependencies, and a set of functions to invoke once all the // dependencies have been initialized. // // This definition corresponds to the dig.Container and dig.Scope. type container interface { Invoke(any, ...dig.InvokeOption) error Provide(any, ...dig.ProvideOption) error Decorate(any, ...dig.DecorateOption) error } // Module is a named group of zero or more fx.Options. // // A Module scopes the effect of certain operations to within the module. // For more information, see [Decorate], [Replace], or [Invoke]. // // Module allows packages to bundle sophisticated functionality into easy-to-use // logical units. // For example, a logging package might export a simple option like this: // // package logging // // var Module = fx.Module("logging", // fx.Provide(func() *log.Logger { // return log.New(os.Stdout, "", 0) // }), // // ... // ) // // A shared all-in-one microservice package could use Module to bundle // all required components of a microservice: // // package server // // var Module = fx.Module("server", // logging.Module, // metrics.Module, // tracing.Module, // rpc.Module, // ) // // When new global functionality is added to the service ecosystem, // it can be added to the shared module with minimal churn for users. // // Use the all-in-one pattern sparingly. // It limits the flexibility available to the application. func Module(name string, opts ...Option) Option { mo := moduleOption{ name: name, location: fxreflect.CallerStack(1, 2)[0], options: opts, } return mo } type moduleOption struct { name string location fxreflect.Frame options []Option } func (o moduleOption) String() string { return fmt.Sprintf("fx.Module(%q, %v)", o.name, o.options) } func (o moduleOption) apply(mod *module) { // This get called on any submodules' that are declared // as part of another module. // 1. Create a new module with the parent being the specified // module. // 2. Apply child Options on the new module. // 3. Append it to the parent module. // Create trace as parent's trace with this module's location pre-pended. trace := append([]string{fmt.Sprintf("%v (%v)", o.location, o.name)}, mod.trace...) newModule := &module{ name: o.name, parent: mod, trace: trace, app: mod.app, } for _, opt := range o.options { opt.apply(newModule) } mod.modules = append(mod.modules, newModule) } type module struct { parent *module name string trace []string scope scope provides []provide invokes []invoke decorators []decorator modules []*module app *App log fxevent.Logger fallbackLogger fxevent.Logger logConstructor *provide } // scope is a private wrapper interface for dig.Container and dig.Scope. // We can consider moving this into Fx using type constraints after Go 1.20 // is released and 1.17 is deprecated. type scope interface { Decorate(f any, opts ...dig.DecorateOption) error Invoke(f any, opts ...dig.InvokeOption) error Provide(f any, opts ...dig.ProvideOption) error Scope(name string, opts ...dig.ScopeOption) *dig.Scope String() string } // builds the Scopes using the App's Container. Note that this happens // after applyModules' are called because the App's Container needs to // be built for any Scopes to be initialized, and applys' should be called // before the Container can get initialized. func (m *module) build(app *App, root *dig.Container) { if m.parent == nil { m.scope = root } else { parentScope := m.parent.scope m.scope = parentScope.Scope(m.name) // use parent module's logger by default m.log = m.parent.log } if m.logConstructor != nil { // Since user supplied a custom logger, use a buffered logger // to hold all messages until user supplied logger is // instantiated. Then we flush those messages after fully // constructing the custom logger. m.fallbackLogger, m.log = m.log, new(logBuffer) } for _, mod := range m.modules { mod.build(app, root) } } func (m *module) provideAll() { for _, p := range m.provides { m.provide(p) } for _, m := range m.modules { m.provideAll() } } func (m *module) provide(p provide) { if m.app.err != nil { return } if p.IsSupply { m.supply(p) return } funcName := fxreflect.FuncName(p.Target) var info dig.ProvideInfo opts := []dig.ProvideOption{ dig.FillProvideInfo(&info), dig.Export(!p.Private), dig.WithProviderBeforeCallback(func(bci dig.BeforeCallbackInfo) { m.log.LogEvent(&fxevent.BeforeRun{ Name: funcName, Kind: "provide", ModuleName: m.name, }) }), dig.WithProviderCallback(func(ci dig.CallbackInfo) { m.log.LogEvent(&fxevent.Run{ Name: funcName, Kind: "provide", ModuleName: m.name, Runtime: ci.Runtime, Err: ci.Error, }) }), } if err := runProvide(m.scope, p, opts...); err != nil { m.app.err = err } outputNames := make([]string, len(info.Outputs)) for i, o := range info.Outputs { outputNames[i] = o.String() } m.log.LogEvent(&fxevent.Provided{ ConstructorName: funcName, StackTrace: p.Stack.Strings(), ModuleTrace: append([]string{p.Stack[0].String()}, m.trace...), ModuleName: m.name, OutputTypeNames: outputNames, Err: m.app.err, Private: p.Private, }) } func (m *module) supply(p provide) { typeName := p.SupplyType.String() opts := []dig.ProvideOption{ dig.Export(!p.Private), dig.WithProviderBeforeCallback(func(bci dig.BeforeCallbackInfo) { m.log.LogEvent(&fxevent.BeforeRun{ Name: fmt.Sprintf("stub(%v)", typeName), Kind: "supply", ModuleName: m.name, }) }), dig.WithProviderCallback(func(ci dig.CallbackInfo) { m.log.LogEvent(&fxevent.Run{ Name: fmt.Sprintf("stub(%v)", typeName), Kind: "supply", Runtime: ci.Runtime, ModuleName: m.name, }) }), } if err := runProvide(m.scope, p, opts...); err != nil { m.app.err = err } m.log.LogEvent(&fxevent.Supplied{ TypeName: typeName, StackTrace: p.Stack.Strings(), ModuleTrace: append([]string{p.Stack[0].String()}, m.trace...), ModuleName: m.name, Err: m.app.err, }) } // Constructs custom loggers for all modules in the tree func (m *module) installAllEventLoggers() { if m.logConstructor != nil { if buffer, ok := m.log.(*logBuffer); ok { // default to parent's logger if custom logger constructor fails if err := m.installEventLogger(buffer); err != nil { m.app.err = multierr.Append(m.app.err, err) m.log = m.fallbackLogger buffer.Connect(m.log) } } m.fallbackLogger = nil } else if m.parent != nil { m.log = m.parent.log } for _, mod := range m.modules { mod.installAllEventLoggers() } } func (m *module) installEventLogger(buffer *logBuffer) (err error) { p := m.logConstructor fname := fxreflect.FuncName(p.Target) defer func() { m.log.LogEvent(&fxevent.LoggerInitialized{ Err: err, ConstructorName: fname, }) }() // TODO: Use dig.FillProvideInfo to inspect the provided constructor // and fail the application if its signature didn't match. if err := m.scope.Provide(p.Target); err != nil { return fmt.Errorf("fx.WithLogger(%v) from:\n%+v\nin Module: %q\nFailed: %w", fname, p.Stack, m.name, err) } return m.scope.Invoke(func(log fxevent.Logger) { m.log = log buffer.Connect(log) }) } func (m *module) invokeAll() error { for _, m := range m.modules { if err := m.invokeAll(); err != nil { return err } } for _, invoke := range m.invokes { if err := m.invoke(invoke); err != nil { return err } } return nil } func (m *module) invoke(i invoke) (err error) { fnName := fxreflect.FuncName(i.Target) m.log.LogEvent(&fxevent.Invoking{ FunctionName: fnName, ModuleName: m.name, }) err = runInvoke(m.scope, i) m.log.LogEvent(&fxevent.Invoked{ FunctionName: fnName, ModuleName: m.name, Err: err, Trace: fmt.Sprintf("%+v", i.Stack), // format stack trace as multi-line }) return err } func (m *module) decorateAll() error { for _, d := range m.decorators { if err := m.decorate(d); err != nil { return err } } for _, m := range m.modules { if err := m.decorateAll(); err != nil { return err } } return nil } func (m *module) decorate(d decorator) (err error) { if d.IsReplace { return m.replace(d) } funcName := fxreflect.FuncName(d.Target) var info dig.DecorateInfo opts := []dig.DecorateOption{ dig.FillDecorateInfo(&info), dig.WithDecoratorBeforeCallback(func(bci dig.BeforeCallbackInfo) { m.log.LogEvent(&fxevent.BeforeRun{ Name: funcName, Kind: "decorate", ModuleName: m.name, }) }), dig.WithDecoratorCallback(func(ci dig.CallbackInfo) { m.log.LogEvent(&fxevent.Run{ Name: funcName, Kind: "decorate", ModuleName: m.name, Runtime: ci.Runtime, Err: ci.Error, }) }), } err = runDecorator(m.scope, d, opts...) outputNames := make([]string, len(info.Outputs)) for i, o := range info.Outputs { outputNames[i] = o.String() } m.log.LogEvent(&fxevent.Decorated{ DecoratorName: funcName, StackTrace: d.Stack.Strings(), ModuleTrace: append([]string{d.Stack[0].String()}, m.trace...), ModuleName: m.name, OutputTypeNames: outputNames, Err: err, }) return err } func (m *module) replace(d decorator) error { typeName := d.ReplaceType.String() opts := []dig.DecorateOption{ dig.WithDecoratorBeforeCallback(func(bci dig.BeforeCallbackInfo) { m.log.LogEvent(&fxevent.BeforeRun{ Name: fmt.Sprintf("stub(%v)", typeName), Kind: "replace", ModuleName: m.name, }) }), dig.WithDecoratorCallback(func(ci dig.CallbackInfo) { m.log.LogEvent(&fxevent.Run{ Name: fmt.Sprintf("stub(%v)", typeName), Kind: "replace", ModuleName: m.name, Runtime: ci.Runtime, Err: ci.Error, }) }), } err := runDecorator(m.scope, d, opts...) m.log.LogEvent(&fxevent.Replaced{ ModuleName: m.name, StackTrace: d.Stack.Strings(), ModuleTrace: append([]string{d.Stack[0].String()}, m.trace...), OutputTypeNames: []string{typeName}, Err: err, }) return err } ================================================ FILE: module_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx_test import ( "bytes" "errors" "log" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/fx/fxtest" "go.uber.org/fx/internal/fxlog" "go.uber.org/zap" ) func TestModuleSuccess(t *testing.T) { t.Parallel() type Logger struct { Name string } type Foo struct { Name string } t.Run("provide a dependency from a submodule", func(t *testing.T) { t.Parallel() redis := fx.Module("redis", fx.Provide(func() *Logger { return &Logger{Name: "redis"} }), ) app := fxtest.New(t, redis, fx.Invoke(func(l *Logger) { assert.Equal(t, "redis", l.Name) }), ) defer app.RequireStart().RequireStop() }) t.Run("provide a dependency from nested modules", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Module("child", fx.Module("grandchild", fx.Provide(func() *Logger { return &Logger{Name: "redis"} }), ), ), fx.Invoke(func(l *Logger) { assert.Equal(t, "redis", l.Name) }), ) defer app.RequireStart().RequireStop() }) t.Run("provide a value to a soft group value from nested modules", func(t *testing.T) { t.Parallel() type Param struct { fx.In Foos []string `group:"foo,soft"` Bar int } type Result struct { fx.Out Foo string `group:"foo"` Bar int } app := fxtest.New(t, fx.Module("child", fx.Module("grandchild", fx.Provide(fx.Annotate( func() string { require.FailNow(t, "should not be called") return "there" }, fx.ResultTags(`group:"foo"`), )), fx.Provide(func() Result { return Result{Foo: "hello", Bar: 10} }), ), ), fx.Invoke(func(p Param) { assert.ElementsMatch(t, []string{"hello"}, p.Foos) }), ) defer app.RequireStart().RequireStop() require.NoError(t, app.Err()) }) t.Run("invoke from nested module", func(t *testing.T) { t.Parallel() invokeRan := false app := fxtest.New(t, fx.Provide(func() *Logger { return &Logger{ Name: "redis", } }), fx.Module("child", fx.Module("grandchild", fx.Invoke(func(l *Logger) { assert.Equal(t, "redis", l.Name) invokeRan = true }), ), ), ) require.True(t, invokeRan) require.NoError(t, app.Err()) defer app.RequireStart().RequireStop() }) t.Run("invoke in module with dep from top module", func(t *testing.T) { t.Parallel() child := fx.Module("child", fx.Invoke(func(l *Logger) { assert.Equal(t, "my logger", l.Name) }), ) app := fxtest.New(t, child, fx.Provide(func() *Logger { return &Logger{Name: "my logger"} }), ) defer app.RequireStart().RequireStop() }) t.Run("provide in module with annotate", func(t *testing.T) { t.Parallel() child := fx.Module("child", fx.Provide(fx.Annotate(func() *Logger { return &Logger{Name: "good logger"} }, fx.ResultTags(`name:"goodLogger"`))), ) app := fxtest.New(t, child, fx.Invoke(fx.Annotate(func(l *Logger) { assert.Equal(t, "good logger", l.Name) }, fx.ParamTags(`name:"goodLogger"`))), ) defer app.RequireStart().RequireStop() }) t.Run("invoke in module with annotate", func(t *testing.T) { t.Parallel() ranInvoke := false child := fx.Module("child", // use something provided by the root module. fx.Invoke(fx.Annotate(func(l *Logger) { assert.Equal(t, "good logger", l.Name) ranInvoke = true })), ) app := fxtest.New(t, child, fx.Provide(fx.Annotate(func() *Logger { return &Logger{Name: "good logger"} })), ) defer app.RequireStart().RequireStop() assert.True(t, ranInvoke) }) t.Run("use Options in Module", func(t *testing.T) { t.Parallel() opts := fx.Options( fx.Provide(fx.Annotate(func() string { return "dog" }, fx.ResultTags(`group:"pets"`))), fx.Provide(fx.Annotate(func() string { return "cat" }, fx.ResultTags(`group:"pets"`))), ) petModule := fx.Module("pets", opts) app := fxtest.New(t, petModule, fx.Invoke(fx.Annotate(func(pets []string) { assert.ElementsMatch(t, []string{"dog", "cat"}, pets) }, fx.ParamTags(`group:"pets"`))), ) defer app.RequireStart().RequireStop() }) t.Run("Invoke order in Modules", func(t *testing.T) { t.Parallel() type person struct { age int } app := fxtest.New(t, fx.Provide(func() *person { return &person{ age: 1, } }), fx.Invoke(func(p *person) { assert.Equal(t, 2, p.age) p.age++ }), fx.Module("module", fx.Invoke(func(p *person) { assert.Equal(t, 1, p.age) p.age++ }), ), fx.Invoke(func(p *person) { assert.Equal(t, 3, p.age) }), ) require.NoError(t, app.Err()) }) t.Run("custom logger for module", func(t *testing.T) { t.Parallel() tests := []struct { desc string giveWithLogger fx.Option wantEvents []string }{ { desc: "custom logger for module", giveWithLogger: fx.NopLogger, wantEvents: []string{ "Provided", "Provided", "Provided", "Supplied", "BeforeRun", "Run", "LoggerInitialized", "Invoking", "Invoked", }, }, { desc: "Not using a custom logger for module defaults to app logger", giveWithLogger: fx.Options(), wantEvents: []string{ "Provided", "Provided", "Provided", "Supplied", "Provided", "BeforeRun", "Run", "LoggerInitialized", "Invoking", "BeforeRun", "Run", "Invoked", "Invoking", "Invoked", }, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() var spy fxlog.Spy redis := fx.Module("redis", fx.Provide(func() *Foo { return &Foo{Name: "redis"} }), fx.Invoke(func(r *Foo) { assert.Equal(t, "redis", r.Name) }), tt.giveWithLogger, ) app := fxtest.New(t, redis, fx.Supply(&spy), fx.WithLogger(func(spy *fxlog.Spy) fxevent.Logger { return spy }), fx.Invoke(func(r *Foo) { assert.Equal(t, "redis", r.Name) }), ) // events from module with a custom logger not logged in app logger assert.Equal(t, tt.wantEvents, spy.EventTypes()) spy.Reset() app.RequireStart().RequireStop() require.NoError(t, app.Err()) assert.Equal(t, []string{"Started", "Stopped"}, spy.EventTypes()) }) } }) type NamedSpy struct { fxlog.Spy name string } t.Run("decorator on module logger does not affect app logger", func(t *testing.T) { t.Parallel() appSpy := NamedSpy{name: "app"} moduleSpy := NamedSpy{name: "redis"} redis := fx.Module("redis", fx.Provide(func() *Foo { return &Foo{Name: "redis"} }), fx.Supply(&appSpy), fx.Replace(&moduleSpy), fx.WithLogger(func(spy *NamedSpy) fxevent.Logger { assert.Equal(t, "redis", spy.name) return spy }), fx.Invoke(func(r *Foo) { assert.Equal(t, "redis", r.Name) }), ) app := fxtest.New(t, redis, fx.WithLogger(func(spy *NamedSpy) fxevent.Logger { assert.Equal(t, "app", spy.name) return spy }), fx.Invoke(func(r *Foo) { assert.Equal(t, "redis", r.Name) }), ) assert.Equal(t, []string{ "Provided", "Supplied", "Replaced", "BeforeRun", "Run", "BeforeRun", "Run", "LoggerInitialized", "Invoking", "BeforeRun", "Run", "Invoked", }, moduleSpy.EventTypes()) assert.Equal(t, []string{ "Provided", "Provided", "Provided", "LoggerInitialized", "Invoking", "Invoked", }, appSpy.EventTypes()) appSpy.Reset() moduleSpy.Reset() app.RequireStart().RequireStop() require.NoError(t, app.Err()) assert.Equal(t, []string{"Started", "Stopped"}, appSpy.EventTypes()) assert.Empty(t, moduleSpy.EventTypes()) }) t.Run("module uses parent module's logger to log events", func(t *testing.T) { t.Parallel() appSpy := NamedSpy{name: "app"} childSpy := NamedSpy{name: "child"} grandchild := fx.Module("grandchild", fx.Provide(func() *Foo { return &Foo{Name: "grandchild"} }), fx.Invoke(func(r *Foo) { assert.Equal(t, "grandchild", r.Name) }), ) child := fx.Module("child", grandchild, fx.Supply(&appSpy), fx.Replace(&childSpy), fx.WithLogger(func(spy *NamedSpy) fxevent.Logger { assert.Equal(t, "child", spy.name) return spy }), fx.Invoke(func(r *Foo) { assert.Equal(t, "grandchild", r.Name) }), ) app := fxtest.New(t, child, fx.WithLogger(func(spy *NamedSpy) fxevent.Logger { assert.Equal(t, "app", spy.name) return spy }), fx.Invoke(func(r *Foo) { assert.Equal(t, "grandchild", r.Name) }), ) assert.Equal(t, []string{ "Supplied", "Provided", "Replaced", "BeforeRun", "Run", "BeforeRun", "Run", "LoggerInitialized", // Invoke logged twice, once from child and another from grandchild "Invoking", "BeforeRun", "Run", "Invoked", "Invoking", "Invoked", }, childSpy.EventTypes(), "events from grandchild also logged in child logger") assert.Equal(t, []string{ "Provided", "Provided", "Provided", "LoggerInitialized", "Invoking", "Invoked", }, appSpy.EventTypes(), "events from modules do not appear in app logger") appSpy.Reset() childSpy.Reset() app.RequireStart().RequireStop() require.NoError(t, app.Err()) assert.Equal(t, []string{"Started", "Stopped"}, appSpy.EventTypes()) assert.Empty(t, childSpy.EventTypes()) }) } func TestModuleFailures(t *testing.T) { t.Parallel() t.Run("invoke from submodule failed", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} sub := fx.Module("sub", fx.Provide(func() *A { return &A{} }), fx.Invoke(func(*A, *B) { // missing dependency. require.Fail(t, "this should not be called") }), ) app := NewForTest(t, sub, fx.Invoke(func(a *A) { assert.NotNil(t, a) }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "missing type: *fx_test.B") }) t.Run("provide the same dependency from multiple modules", func(t *testing.T) { t.Parallel() type A struct{} app := NewForTest(t, fx.Module("mod1", fx.Provide(func() A { return A{} })), fx.Module("mod2", fx.Provide(func() A { return A{} })), fx.Invoke(func(a A) {}), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "already provided by ") }) t.Run("providing Modules should fail", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Module("module", fx.Provide( fx.Module("module"), ), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "fx.Option should be passed to fx.New directly, not to fx.Provide") }) t.Run("invoking Modules should fail", func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Module("module", fx.Invoke( fx.Invoke("module"), ), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "fx.Option should be passed to fx.New directly, not to fx.Invoke") }) t.Run("annotate failure in Module", func(t *testing.T) { t.Parallel() type A struct{} newA := func() A { return A{} } app := NewForTest(t, fx.Module("module", fx.Provide(fx.Annotate(newA, fx.ParamTags(`name:"A1"`), fx.ParamTags(`name:"A2"`), )), ), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "encountered error while applying annotation") assert.Contains(t, err.Error(), "cannot apply more than one line of ParamTags") }) t.Run("soft provided to fx.Out struct", func(t *testing.T) { t.Parallel() type Result struct { fx.Out Bars []int `group:"bar,soft"` } app := NewForTest(t, fx.Provide(func() Result { return Result{Bars: []int{1, 2, 3}} }), ) err := app.Err() require.Error(t, err, "failed to create app") assert.Contains(t, err.Error(), "cannot use soft with result value groups") }) t.Run("provider in Module fails", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} newA := func() (A, error) { return A{}, nil } newB := func() (B, error) { return B{}, errors.New("minor sadness") } app := NewForTest(t, fx.Module("module", fx.Provide( newA, newB, ), ), fx.Invoke(func(A, B) { assert.Fail(t, "this should never run") }), ) err := app.Err() require.Error(t, err) assert.Contains(t, err.Error(), "failed to build fx_test.B") assert.Contains(t, err.Error(), "minor sadness") }) t.Run("invalid Options in Module", func(t *testing.T) { t.Parallel() tests := []struct { desc string opt fx.Option }{ { desc: "StartTimeout Option", opt: fx.StartTimeout(time.Second), }, { desc: "StopTimeout Option", opt: fx.StopTimeout(time.Second), }, { desc: "Logger Option", opt: fx.Logger(log.New(&bytes.Buffer{}, "", 0)), }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { t.Parallel() app := NewForTest(t, fx.Module("module", tt.opt, ), ) require.Error(t, app.Err()) }) } }) t.Run("invalid WithLogger in Module", func(t *testing.T) { t.Parallel() var spy fxlog.Spy spyAsLogger := fx.Options( fx.Supply(&spy), fx.WithLogger(func(spy *fxlog.Spy) fxevent.Logger { return spy }), ) defaultModuleOpts := fx.Options( fx.Provide(func() string { return "redis" }), fx.Invoke(func(s string) { assert.Fail(t, "this should never run") }), ) tests := []struct { desc string giveModuleOpts fx.Option giveAppOpts fx.Option wantErrContains []string wantEvents []string }{ { desc: "error in Provide shows logs in module", giveModuleOpts: fx.Options(spyAsLogger, fx.Provide(&bytes.Buffer{})), // not passing in a constructor giveAppOpts: fx.Options(), wantErrContains: []string{ "must provide constructor function, got (type *bytes.Buffer)", }, wantEvents: []string{ "Supplied", "Provided", "BeforeRun", "Run", "LoggerInitialized", }, }, { desc: "logger in module failed to build", giveModuleOpts: fx.WithLogger(func() (fxevent.Logger, error) { return nil, errors.New("error building logger") }), giveAppOpts: spyAsLogger, wantErrContains: []string{"error building logger"}, wantEvents: []string{ "Provided", "Provided", "Provided", "Supplied", "BeforeRun", "Run", "LoggerInitialized", "Provided", "LoggerInitialized", }, }, { desc: "logger dependency in module failed to build", giveModuleOpts: fx.Options( fx.Provide(func() (*zap.Logger, error) { return nil, errors.New("error building logger dependency") }), fx.WithLogger(func(log *zap.Logger) fxevent.Logger { t.Errorf("WithLogger must not be called") panic("must not be called") }), ), giveAppOpts: spyAsLogger, wantErrContains: []string{"error building logger dependency"}, wantEvents: []string{ "Provided", "Provided", "Provided", "Supplied", "BeforeRun", "Run", "LoggerInitialized", "Provided", "Provided", "BeforeRun", "Run", "LoggerInitialized", }, }, { desc: "Invalid input for WithLogger", giveModuleOpts: fx.WithLogger(&fxlog.Spy{}), // not passing in a constructor for WithLogger giveAppOpts: spyAsLogger, wantErrContains: []string{ "fx.WithLogger", "from:", "Failed", }, wantEvents: []string{ "Provided", "Provided", "Provided", "Supplied", "BeforeRun", "Run", "LoggerInitialized", "Provided", "LoggerInitialized", }, }, } for _, tt := range tests { spy.Reset() t.Run(tt.desc, func(t *testing.T) { redis := fx.Module("redis", tt.giveModuleOpts, defaultModuleOpts, ) app := fx.New( tt.giveAppOpts, redis, fx.Invoke(func(s string) { assert.Fail(t, "this should never run") }), ) err := app.Err() require.Error(t, err) for _, contains := range tt.wantErrContains { assert.Contains(t, err.Error(), contains) } assert.Equal(t, tt.wantEvents, spy.EventTypes(), ) }) } }) } ================================================ FILE: populate.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "reflect" ) // Populate sets targets with values from the dependency injection container // during application initialization. All targets must be pointers to the // values that must be populated. Pointers to structs that embed In are // supported, which can be used to populate multiple values in a struct. // // Annotating each pointer with ParamTags is also supported as a shorthand // to passing a pointer to a struct that embeds In with field tags. For example: // // var a A // var b B // fx.Populate( // fx.Annotate( // &a, // fx.ParamTags(`name:"A"`) // ), // fx.Annotate( // &b, // fx.ParamTags(`name:"B"`) // ) // ) // // Code above is equivalent to the following: // // type Target struct { // fx.In // // a A `name:"A"` // b B `name:"B"` // } // var target Target // ... // fx.Populate(&target) // // This is most helpful in unit tests: it lets tests leverage Fx's automatic // constructor wiring to build a few structs, but then extract those structs // for further testing. func Populate(targets ...any) Option { // Validate all targets are non-nil pointers. fields := make([]reflect.StructField, len(targets)+1) fields[0] = reflect.StructField{ Name: "In", Type: reflect.TypeOf(In{}), Anonymous: true, } for i, t := range targets { if t == nil { return Error(fmt.Errorf("failed to Populate: target %v is nil", i+1)) } var ( rt reflect.Type tag reflect.StructTag ) switch t := t.(type) { case annotated: rt = reflect.TypeOf(t.Target) tag = reflect.StructTag(t.ParamTags[0]) targets[i] = t.Target default: rt = reflect.TypeOf(t) } if rt.Kind() != reflect.Ptr { return Error(fmt.Errorf("failed to Populate: target %v is not a pointer type, got %T", i+1, t)) } fields[i+1] = reflect.StructField{ Name: fmt.Sprintf("Field%d", i), Type: rt.Elem(), Tag: tag, } } // Build a function that looks like: // // func(t1 T1, t2 T2, ...) { // *targets[0] = t1 // *targets[1] = t2 // [...] // } // fnType := reflect.FuncOf([]reflect.Type{reflect.StructOf(fields)}, nil, false /* variadic */) fn := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value { arg := args[0] for i, target := range targets { reflect.ValueOf(target).Elem().Set(arg.Field(i + 1)) } return nil }) return Invoke(fn.Interface()) } ================================================ FILE: populate_example_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx_test import ( "context" "fmt" "go.uber.org/fx" ) func ExamplePopulate() { // Some external module that provides a user name. type Username string UserModule := fx.Provide(func() Username { return "john" }) // We want to use Fx to wire up our constructors, but don't actually want to // run the application - we just want to yank out the user name. // // This is common in unit tests, and is even easier with the fxtest // package's RequireStart and RequireStop helpers. var user Username app := fx.New( UserModule, fx.NopLogger, // silence test output fx.Populate(&user), ) if err := app.Start(context.Background()); err != nil { panic(err) } defer app.Stop(context.Background()) fmt.Println(user) // Output: // john } ================================================ FILE: populate_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx_test import ( "io" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" . "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestPopulate(t *testing.T) { t.Parallel() // We make sure t1 has a size so when we compare pointers, 2 different // objects are not equal. type t1 struct { buf [1024]byte } type t2 struct{} _ = new(t1).buf // buf is unused t.Run("populate nothing", func(t *testing.T) { t.Parallel() app := fxtest.New(t, Provide(func() *t1 { panic("should not be called ") }), Populate(), ) app.RequireStart().RequireStop() }) t.Run("populate single", func(t *testing.T) { t.Parallel() var v1 *t1 app := fxtest.New(t, Provide(func() *t1 { return &t1{} }), Populate(&v1), ) app.RequireStart().RequireStop() require.NotNil(t, v1, "did not populate value") }) t.Run("populate interface", func(t *testing.T) { t.Parallel() var reader io.Reader app := fxtest.New(t, Provide(func() io.Reader { return strings.NewReader("hello world") }), Populate(&reader), ) app.RequireStart().RequireStop() bs, err := io.ReadAll(reader) require.NoError(t, err, "Failed to use populated io.Reader") assert.Equal(t, "hello world", string(bs), "Unexpected reader") }) t.Run("populate multiple inline values", func(t *testing.T) { t.Parallel() var ( v1 *t1 v2 *t2 ) app := fxtest.New(t, Provide(func() *t1 { return &t1{} }), Provide(func() *t2 { return &t2{} }), Populate(&v1), Populate(&v2), ) app.RequireStart().RequireStop() require.NotNil(t, v1, "did not populate argument 1") require.NotNil(t, v2, "did not populate argument 2") }) t.Run("populate fx.In struct", func(t *testing.T) { t.Parallel() targets := struct { In V1 *t1 V2 *t2 }{} app := fxtest.New(t, Provide(func() *t1 { return &t1{} }), Provide(func() *t2 { return &t2{} }), Populate(&targets), ) app.RequireStart().RequireStop() require.NotNil(t, targets.V1, "did not populate field 1") require.NotNil(t, targets.V2, "did not populate field 2") }) t.Run("populate named field", func(t *testing.T) { t.Parallel() type result struct { Out V1 *t1 `name:"n1"` V2 *t1 `name:"n2"` } targets := struct { In V1 *t1 `name:"n1"` V2 *t1 `name:"n2"` }{} app := fxtest.New(t, Provide(func() result { return result{ V1: &t1{}, V2: &t1{}, } }), Populate(&targets), ) app.RequireStart().RequireStop() require.NotNil(t, targets.V1, "did not populate field 1") require.NotNil(t, targets.V2, "did not populate field 2") // Cannot use assert.Equal here as we want to compare pointers. assert.False(t, targets.V1 == targets.V2, "fields should be different") }) t.Run("annotated populate", func(t *testing.T) { t.Parallel() type result struct { Out V1 *t1 `name:"n1"` V2 *t1 `name:"n2"` } var v1, v2 *t1 app := fxtest.New(t, Provide(func() result { return result{ V1: &t1{}, V2: &t1{}, } }), Populate( Annotate( &v1, ParamTags(`name:"n1"`), ), Annotate( &v2, ParamTags(`name:"n2"`), ), ), ) app.RequireStart().RequireStop() require.NotNil(t, v1, "did not populate argument 1") require.NotNil(t, v2, "did not populate argument 2") // Cannot use assert.Equal here as we want to compare pointers. assert.False(t, v1 == v2, "values should be different") }) t.Run("annotated populate with annotated provide", func(t *testing.T) { t.Parallel() var v1, v2 *t1 app := fxtest.New(t, Provide( Annotate( func() (*t1, *t1) { return &t1{}, &t1{} }, ResultTags(`name:"n1"`, `name:"n2"`), ), ), Populate( Annotate( &v1, ParamTags(`name:"n1"`), ), Annotate( &v2, ParamTags(`name:"n2"`), ), ), ) app.RequireStart().RequireStop() require.NotNil(t, v1, "did not populate argument 1") require.NotNil(t, v2, "did not populate argument 2") // Cannot use assert.Equal here as we want to compare pointers. assert.False(t, v1 == v2, "values should be different") }) t.Run("populate group", func(t *testing.T) { t.Parallel() type result struct { Out V1 *t1 `group:"g"` V2 *t1 `group:"g"` } targets := struct { In Group []*t1 `group:"g"` }{} app := fxtest.New(t, Provide(func() result { return result{ V1: &t1{}, V2: &t1{}, } }), Populate(&targets), ) app.RequireStart().RequireStop() require.Len(t, targets.Group, 2, "Expected group to have 2 values") require.NotNil(t, targets.Group[0], "did not populate group value 1") require.NotNil(t, targets.Group[1], "did not populate group value 2") // Cannot use assert.Equal here as we want to compare pointers. assert.False(t, targets.Group[0] == targets.Group[1], "group values should be different") }) } func TestPopulateErrors(t *testing.T) { t.Parallel() type t1 struct{} type container struct { In T1 t1 } type containerNoIn struct { T1 t1 } fn := func() {} var v *t1 tests := []struct { msg string opt Option wantErr string }{ { msg: "inline value", opt: Populate(t1{}), wantErr: "not a pointer", }, { msg: "container value", opt: Populate(container{}), wantErr: "not a pointer", }, { msg: "container pointer without fx.In", opt: Populate(&containerNoIn{}), wantErr: "missing type: fx_test.containerNoIn", }, { msg: "function", opt: Populate(fn), wantErr: "not a pointer", }, { msg: "function pointer", opt: Populate(&fn), wantErr: "missing type: func()", }, { msg: "invalid last argument", opt: Populate(&v, t1{}), wantErr: "target 2 is not a pointer type", }, { msg: "nil argument", opt: Populate(&v, nil, &v), wantErr: "target 2 is nil", }, } for _, tt := range tests { app := NewForTest(t, NopLogger, Provide(func() *t1 { return &t1{} }), tt.opt, ) require.Error(t, app.Err()) assert.Contains(t, app.Err().Error(), tt.wantErr) } } func TestPopulateValidateApp(t *testing.T) { t.Parallel() type t1 struct{} type container struct { In T1 t1 } type containerNoIn struct { T1 t1 } fn := func() {} var v *t1 tests := []struct { msg string opts []any wantErr string }{ { msg: "inline value", opts: []any{t1{}}, wantErr: "not a pointer", }, { msg: "container value", opts: []any{container{}}, wantErr: "not a pointer", }, { msg: "container pointer without fx.In", opts: []any{&containerNoIn{}}, wantErr: "missing type: fx_test.containerNoIn", }, { msg: "function", opts: []any{fn}, wantErr: "not a pointer", }, { msg: "function pointer", opts: []any{&fn}, wantErr: "missing type: func()", }, { msg: "invalid last argument", opts: []any{&v, t1{}}, wantErr: "target 2 is not a pointer type", }, { msg: "nil argument", opts: []any{&v, nil, &v}, wantErr: "target 2 is nil", }, } for _, tt := range tests { t.Run(tt.msg, func(t *testing.T) { t.Parallel() testOpts := []Option{ NopLogger, Provide(func() *t1 { return &t1{} }), Populate(tt.opts...), } err := validateTestApp(t, testOpts...) require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) }) } } ================================================ FILE: printer_writer.go ================================================ // Copyright (c) 2020-2021 Uber Technologies, Inc. // // 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. package fx import "io" type printerWriter struct{ p Printer } // writerFromPrinter returns an implementation of io.Writer used to support // Logger option which implements Printer interface. func writerFromPrinter(p Printer) io.Writer { return &printerWriter{p: p} } func (w *printerWriter) Write(b []byte) (n int, err error) { w.p.Printf(string(b)) return len(b), nil } ================================================ FILE: provide.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "reflect" "strings" "go.uber.org/dig" "go.uber.org/fx/internal/fxreflect" ) // Provide registers any number of constructor functions, teaching the // application how to instantiate various types. The supplied constructor // function(s) may depend on other types available in the application, must // return one or more objects, and may return an error. For example: // // // Constructs type *C, depends on *A and *B. // func(*A, *B) *C // // // Constructs type *C, depends on *A and *B, and indicates failure by // // returning an error. // func(*A, *B) (*C, error) // // // Constructs types *B and *C, depends on *A, and can fail. // func(*A) (*B, *C, error) // // The order in which constructors are provided doesn't matter, and passing // multiple Provide options appends to the application's collection of // constructors. Constructors are called only if one or more of their returned // types are needed, and their results are cached for reuse (so instances of a // type are effectively singletons within an application). Taken together, // these properties make it perfectly reasonable to Provide a large number of // constructors even if only a fraction of them are used. // // See the documentation of the In and Out types for advanced features, // including optional parameters and named instances. // // See the documentation for [Private] for restricting access to constructors. // // Constructor functions should perform as little external interaction as // possible, and should avoid spawning goroutines. Things like server listen // loops, background timer loops, and background processing goroutines should // instead be managed using Lifecycle callbacks. func Provide(constructors ...any) Option { return provideOption{ Targets: constructors, Stack: fxreflect.CallerStack(1, 0), } } type provideOption struct { Targets []any Stack fxreflect.Stack } func (o provideOption) apply(mod *module) { var private bool targets := make([]any, 0, len(o.Targets)) for _, target := range o.Targets { if _, ok := target.(privateOption); ok { private = true continue } targets = append(targets, target) } for _, target := range targets { mod.provides = append(mod.provides, provide{ Target: target, Stack: o.Stack, Private: private, }) } } type privateOption struct{} // Private is an option that can be passed as an argument to [Provide] or [Supply] to // restrict access to the constructors being provided. Specifically, // corresponding constructors can only be used within the current module // or modules the current module contains. Other modules that contain this // module won't be able to use the constructor. // // For example, the following would fail because the app doesn't have access // to the inner module's constructor. // // fx.New( // fx.Module("SubModule", fx.Provide(func() int { return 0 }, fx.Private)), // fx.Invoke(func(a int) {}), // ) var Private = privateOption{} func (o provideOption) String() string { items := make([]string, len(o.Targets)) for i, c := range o.Targets { items[i] = fxreflect.FuncName(c) } return fmt.Sprintf("fx.Provide(%s)", strings.Join(items, ", ")) } func runProvide(c container, p provide, opts ...dig.ProvideOption) error { constructor := p.Target if _, ok := constructor.(Option); ok { return fmt.Errorf("fx.Option should be passed to fx.New directly, "+ "not to fx.Provide: fx.Provide received %v from:\n%+v", constructor, p.Stack) } switch constructor := constructor.(type) { case annotationError: // fx.Annotate failed. Turn it into an Fx error. return fmt.Errorf( "encountered error while applying annotation using fx.Annotate to %s: %w", fxreflect.FuncName(constructor.target), constructor.err) case annotated: ctor, err := constructor.Build() if err != nil { return fmt.Errorf("fx.Provide(%v) from:\n%+vFailed: %w", constructor, p.Stack, err) } opts = append(opts, dig.LocationForPC(constructor.FuncPtr)) if err := c.Provide(ctor, opts...); err != nil { return fmt.Errorf("fx.Provide(%v) from:\n%+vFailed: %w", constructor, p.Stack, err) } case Annotated: ann := constructor switch { case len(ann.Group) > 0 && len(ann.Name) > 0: return fmt.Errorf( "fx.Annotated may specify only one of Name or Group: received %v from:\n%+v", ann, p.Stack) case len(ann.Name) > 0: opts = append(opts, dig.Name(ann.Name)) case len(ann.Group) > 0: opts = append(opts, dig.Group(ann.Group)) } if err := c.Provide(ann.Target, opts...); err != nil { return fmt.Errorf("fx.Provide(%v) from:\n%+vFailed: %w", ann, p.Stack, err) } default: if reflect.TypeOf(constructor).Kind() == reflect.Func { ft := reflect.ValueOf(constructor).Type() for i := 0; i < ft.NumOut(); i++ { t := ft.Out(i) if t == reflect.TypeOf(Annotated{}) { return fmt.Errorf( "fx.Annotated should be passed to fx.Provide directly, "+ "it should not be returned by the constructor: "+ "fx.Provide received %v from:\n%+v", fxreflect.FuncName(constructor), p.Stack) } } } if err := c.Provide(constructor, opts...); err != nil { return fmt.Errorf("fx.Provide(%v) from:\n%+vFailed: %w", fxreflect.FuncName(constructor), p.Stack, err) } } return nil } ================================================ FILE: replace.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "reflect" "strings" "go.uber.org/fx/internal/fxreflect" ) // Replace provides instantiated values for graph modification as if // they had been provided using a decorator with fx.Decorate. // The most specific type of each value (as determined by reflection) is used. // // Refer to the documentation on fx.Decorate to see how graph modifications // work with fx.Module. // // This serves a purpose similar to what fx.Supply does for fx.Provide. // // For example, given, // // var log *zap.Logger = ... // // The following two forms are equivalent. // // fx.Replace(log) // // fx.Decorate( // func() *zap.Logger { // return log // }, // ) // // Replace panics if a value (or annotation target) is an untyped nil or an error. // // # Replace Caveats // // As mentioned above, Replace uses the most specific type of the provided // value. For interface values, this refers to the type of the implementation, // not the interface. So if you try to replace an io.Writer, fx.Replace will // use the type of the implementation. // // var stderr io.Writer = os.Stderr // fx.Replace(stderr) // // Is equivalent to, // // fx.Decorate(func() *os.File { return os.Stderr }) // // This is typically NOT what you intended. To replace the io.Writer in the // container with the value above, we need to use the fx.Annotate function with // the fx.As annotation. // // fx.Replace( // fx.Annotate(os.Stderr, fx.As(new(io.Writer))) // ) func Replace(values ...any) Option { decorators := make([]any, len(values)) // one function per value types := make([]reflect.Type, len(values)) for i, value := range values { switch value := value.(type) { case annotated: var typ reflect.Type value.Target, typ = newReplaceDecorator(value.Target) decorators[i] = value types[i] = typ default: decorators[i], types[i] = newReplaceDecorator(value) } } return replaceOption{ Targets: decorators, Types: types, Stack: fxreflect.CallerStack(1, 0), } } type replaceOption struct { Targets []any Types []reflect.Type // type of value produced by constructor[i] Stack fxreflect.Stack } func (o replaceOption) apply(m *module) { for i, target := range o.Targets { m.decorators = append(m.decorators, decorator{ Target: target, Stack: o.Stack, IsReplace: true, ReplaceType: o.Types[i], }) } } func (o replaceOption) String() string { items := make([]string, 0, len(o.Targets)) for _, typ := range o.Types { items = append(items, typ.String()) } return fmt.Sprintf("fx.Replace(%s)", strings.Join(items, ", ")) } // Returns a function that takes no parameters, and returns the given value. func newReplaceDecorator(value any) (any, reflect.Type) { switch value.(type) { case nil: panic("untyped nil passed to fx.Replace") case error: panic("error value passed to fx.Replace") } typ := reflect.TypeOf(value) returnTypes := []reflect.Type{typ} returnValues := []reflect.Value{reflect.ValueOf(value)} ft := reflect.FuncOf([]reflect.Type{}, returnTypes, false) fv := reflect.MakeFunc(ft, func([]reflect.Value) []reflect.Value { return returnValues }) return fv.Interface(), typ } ================================================ FILE: replace_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx_test import ( "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestReplaceSuccess(t *testing.T) { t.Parallel() t.Run("replace a value", func(t *testing.T) { t.Parallel() type A struct { Value string } a := &A{Value: "a'"} app := fxtest.New(t, fx.Provide(func() *A { return &A{ Value: "a", } }), fx.Replace(a), fx.Invoke(func(a *A) { assert.Equal(t, "a'", a.Value) }), ) defer app.RequireStart().RequireStop() }) t.Run("replace in a module", func(t *testing.T) { t.Parallel() type A struct { Value string } a := &A{Value: "A"} app := fxtest.New(t, fx.Module("child", fx.Replace(a), fx.Invoke(func(a *A) { assert.Equal(t, "A", a.Value) }), ), fx.Provide(func() *A { return &A{ Value: "a", } }), ) defer app.RequireStart().RequireStop() }) t.Run("replace with annotate", func(t *testing.T) { t.Parallel() type A struct { Value string } app := fxtest.New(t, fx.Supply( fx.Annotate(A{"A"}, fx.ResultTags(`name:"t"`)), ), fx.Replace( fx.Annotate(A{"B"}, fx.ResultTags(`name:"t"`)), ), fx.Invoke(fx.Annotate(func(a A) { assert.Equal(t, a.Value, "B") }, fx.ParamTags(`name:"t"`))), ) defer app.RequireStart().RequireStop() }) t.Run("replace a value group with annotate", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Supply( fx.Annotate([]string{"A", "B", "C"}, fx.ResultTags(`group:"t,flatten"`)), ), fx.Replace(fx.Annotate([]string{"a", "b", "c"}, fx.ResultTags(`group:"t"`))), fx.Invoke(fx.Annotate(func(ss ...string) { assert.ElementsMatch(t, []string{"a", "b", "c"}, ss) }, fx.ParamTags(`group:"t"`))), ) defer app.RequireStart().RequireStop() }) t.Run("replace a value group supplied by a child module from root module", func(t *testing.T) { t.Parallel() foo := fx.Module("foo", fx.Supply( fx.Annotate([]string{"a", "b", "c"}, fx.ResultTags(`group:"t,flatten"`)), ), ) fxtest.New(t, fx.Module("wrapfoo", foo, fx.Replace( fx.Annotate([]string{"d", "e", "f"}, fx.ResultTags(`group:"t"`)), ), fx.Invoke(fx.Annotate(func(ss []string) { assert.ElementsMatch(t, []string{"d", "e", "f"}, ss) }, fx.ParamTags(`group:"t"`))), ), ) }) } func TestReplaceFailure(t *testing.T) { t.Parallel() t.Run("replace same value twice", func(t *testing.T) { t.Parallel() type A struct { Value string } a := &A{Value: "A"} app := NewForTest(t, fx.Provide(func() *A { return &A{Value: "a"} }), fx.Module("child", fx.Replace(a), fx.Replace(a), fx.Invoke(func(a *A) { assert.Fail(t, "this should never run") }), ), ) err := app.Err() assert.Error(t, err) assert.Contains(t, err.Error(), "*fx_test.A already decorated") }) t.Run("replace a value that wasn't provided", func(t *testing.T) { t.Parallel() type A struct{} app := NewForTest(t, fx.Replace(A{}), fx.Invoke(func(a *A) { }), ) err := app.Err() assert.Error(t, err) assert.Contains(t, err.Error(), "missing type: *fx_test.A") }) t.Run("replace panics on invalid values", func(t *testing.T) { t.Parallel() type A struct{} type B struct{} require.PanicsWithValuef( t, "untyped nil passed to fx.Replace", func() { fx.Replace(A{}, nil) }, "a naked nil should panic", ) require.PanicsWithValuef( t, "error value passed to fx.Replace", func() { fx.Replace(A{}, errors.New("some error")) }, "replacing with an error should panic", ) require.NotPanicsf( t, func() { fx.Replace(A{}, (*B)(nil)) }, "a wrapped nil should not panic") }) } ================================================ FILE: shutdown.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx import ( "time" ) // Shutdowner provides a method that can manually trigger the shutdown of the // application by sending a signal to all open Done channels. Shutdowner works // on applications using Run as well as Start, Done, and Stop. The Shutdowner is // provided to all Fx applications. type Shutdowner interface { Shutdown(...ShutdownOption) error } // ShutdownOption provides a way to configure properties of the shutdown // process. Currently, no options have been implemented. type ShutdownOption interface { apply(*shutdowner) } type exitCodeOption int func (code exitCodeOption) apply(s *shutdowner) { s.exitCode = int(code) } var _ ShutdownOption = exitCodeOption(0) // ExitCode is a [ShutdownOption] that may be passed to the Shutdown method of the // [Shutdowner] interface. // The given integer exit code will be broadcasted to any receiver waiting // on a [ShutdownSignal] from the [Wait] method. func ExitCode(code int) ShutdownOption { return exitCodeOption(code) } type shutdownTimeoutOption time.Duration func (shutdownTimeoutOption) apply(*shutdowner) {} var _ ShutdownOption = shutdownTimeoutOption(0) // ShutdownTimeout is a [ShutdownOption] that allows users to specify a timeout // for a given call to Shutdown method of the [Shutdowner] interface. As the // Shutdown method will block while waiting for a signal receiver relay // goroutine to stop. // // Deprecated: This option has no effect. Shutdown is not a blocking operation. func ShutdownTimeout(timeout time.Duration) ShutdownOption { return shutdownTimeoutOption(timeout) } type shutdowner struct { app *App exitCode int } // Shutdown broadcasts a signal to all of the application's Done channels // and begins the Stop process. Applications can be shut down only after they // have finished starting up. func (s *shutdowner) Shutdown(opts ...ShutdownOption) error { for _, opt := range opts { opt.apply(s) } return s.app.receivers.b.Broadcast(ShutdownSignal{ Signal: _sigTERM, ExitCode: s.exitCode, }) } func (app *App) shutdowner() Shutdowner { return &shutdowner{app: app} } ================================================ FILE: shutdown_test.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx_test import ( "context" "fmt" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) func TestShutdown(t *testing.T) { t.Parallel() t.Run("BroadcastsToMultipleChannels", func(t *testing.T) { t.Parallel() var s fx.Shutdowner app := fxtest.New( t, fx.Populate(&s), ) done1, done2 := app.Done(), app.Done() defer app.RequireStart().RequireStop() assert.NoError(t, s.Shutdown(), "error in app shutdown") assert.NotNil(t, <-done1, "done channel 1 did not receive signal") assert.NotNil(t, <-done2, "done channel 2 did not receive signal") }) t.Run("ErrorOnUnsentSignal", func(t *testing.T) { t.Parallel() var s fx.Shutdowner app := fxtest.New( t, fx.Populate(&s), ) done := app.Done() wait := app.Wait() defer app.RequireStart().RequireStop() assert.NoError(t, s.Shutdown(), "error returned from first shutdown call") assert.EqualError(t, s.Shutdown(), "send terminated signal: 2/2 channels are blocked", "unexpected error returned when shutdown is called with a blocked channel") assert.NotNil(t, <-done, "done channel did not receive signal") assert.NotNil(t, <-wait, "wait channel did not receive signal") }) t.Run("shutdown app before calling Done()", func(t *testing.T) { t.Parallel() var s fx.Shutdowner app := fxtest.New( t, fx.Populate(&s), ) require.NoError(t, app.Start(context.Background()), "error starting app") assert.NoError(t, s.Shutdown(), "error in app shutdown") done1, done2 := app.Done(), app.Done() defer app.Stop(context.Background()) // Receiving on done1 and done2 will deadlock in the event that app.Done() // doesn't work as expected. assert.NotNil(t, <-done1, "done channel 1 did not receive signal") assert.NotNil(t, <-done2, "done channel 2 did not receive signal") }) t.Run("with exit code", func(t *testing.T) { t.Parallel() var s fx.Shutdowner app := fxtest.New( t, fx.Populate(&s), ) require.NoError(t, app.Start(context.Background()), "error starting app") assert.NoError(t, s.Shutdown(fx.ExitCode(2)), "error in app shutdown") wait := <-app.Wait() defer app.Stop(context.Background()) require.Equal(t, 2, wait.ExitCode) }) t.Run("with exit code and multiple Wait", func(t *testing.T) { t.Parallel() var s fx.Shutdowner app := fxtest.New( t, fx.Populate(&s), ) require.NoError(t, app.Start(context.Background()), "error starting app") t.Cleanup(func() { app.Stop(context.Background()) }) // in t.Cleanup so this happens after all subtests return (not just this function) defer require.NoError(t, app.Stop(context.Background())) for i := range 10 { t.Run(fmt.Sprintf("Wait %v", i), func(t *testing.T) { t.Parallel() wait := <-app.Wait() require.Equal(t, 2, wait.ExitCode) }) } assert.NoError(t, s.Shutdown(fx.ExitCode(2), fx.ShutdownTimeout(time.Second))) }) t.Run("from invoke", func(t *testing.T) { t.Parallel() app := fxtest.New( t, fx.Invoke(func(s fx.Shutdowner) { s.Shutdown() }), ) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() require.NoError(t, app.Start(ctx), "error starting app") defer app.Stop(ctx) select { case <-ctx.Done(): assert.Fail(t, "app did not shutdown in time") case <-app.Wait(): // success } }) t.Run("many times", func(t *testing.T) { t.Parallel() var shutdowner fx.Shutdowner app := fxtest.New( t, fx.Populate(&shutdowner), ) for i := range 10 { app.RequireStart() shutdowner.Shutdown(fx.ExitCode(i)) assert.Equal(t, i, (<-app.Wait()).ExitCode, "run %d", i) app.RequireStop() } }) } func TestDataRace(t *testing.T) { t.Parallel() var s fx.Shutdowner app := fxtest.New( t, fx.Populate(&s), ) defer app.RequireStart().RequireStop() const N = 50 ready := make(chan struct{}) // used to orchestrate goroutines for Done() and ShutdownOption() var wg sync.WaitGroup // tracks and waits for all goroutines // Spawn N goroutines, each of which call app.Done() and assert // the signal received. wg.Add(N) for i := range N { i := i go func() { defer wg.Done() <-ready done := app.Done() assert.NotNil(t, <-done, "done channel %v did not receive signal", i) }() } // call Shutdown() wg.Add(1) go func() { <-ready defer wg.Done() assert.NoError(t, s.Shutdown(), "error in app shutdown") }() close(ready) wg.Wait() } ================================================ FILE: signal.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx import ( "context" "fmt" "os" "os/signal" "sync" ) // ShutdownSignal represents a signal to be written to Wait or Done. // Should a user call the Shutdown method via the Shutdowner interface with // a provided ExitCode, that exit code will be populated in the ExitCode field. // // Should the application receive an operating system signal, // the Signal field will be populated with the received os.Signal. type ShutdownSignal struct { Signal os.Signal ExitCode int } // String will render a ShutdownSignal type as a string suitable for printing. func (sig ShutdownSignal) String() string { return fmt.Sprintf("%v", sig.Signal) } func newSignalReceivers() signalReceivers { return signalReceivers{ notify: signal.Notify, stopNotify: signal.Stop, signals: make(chan os.Signal, 1), b: &broadcaster{}, } } // signalReceivers listens to OS signals and shutdown signals, // and relays them to registered listeners when started. type signalReceivers struct { // this mutex protects writes and reads of this struct to prevent // race conditions in a parallel execution pattern m sync.Mutex // our os.Signal channel we relay from signals chan os.Signal // when written to, will instruct the signal relayer to shutdown shutdown chan struct{} // is written to when signal relay has finished shutting down finished chan struct{} // this stub allows us to unit test signal relay functionality notify func(c chan<- os.Signal, sig ...os.Signal) stopNotify func(c chan<- os.Signal) // used to register and broadcast to signal listeners // created via Done and Wait b *broadcaster } func (recv *signalReceivers) relayer() { defer func() { recv.finished <- struct{}{} }() select { case <-recv.shutdown: return case signal := <-recv.signals: recv.b.Broadcast(ShutdownSignal{ Signal: signal, }) } } // running returns true if the the signal relay go-routine is running. // this method must be invoked under locked mutex to avoid race condition. func (recv *signalReceivers) running() bool { return recv.shutdown != nil && recv.finished != nil } func (recv *signalReceivers) Start() { recv.m.Lock() defer recv.m.Unlock() // if the receiver has already been started; don't start it again if recv.running() { return } recv.finished = make(chan struct{}, 1) recv.shutdown = make(chan struct{}, 1) recv.notify(recv.signals, os.Interrupt, _sigINT, _sigTERM) go recv.relayer() } func (recv *signalReceivers) Stop(ctx context.Context) error { recv.m.Lock() defer recv.m.Unlock() recv.stopNotify(recv.signals) // if the relayer is not running; return nil error if !recv.running() { return nil } recv.shutdown <- struct{}{} select { case <-ctx.Done(): return ctx.Err() case <-recv.finished: close(recv.shutdown) close(recv.finished) recv.shutdown = nil recv.finished = nil recv.b.reset() return nil } } func (recv *signalReceivers) Done() <-chan os.Signal { return recv.b.Done() } func (recv *signalReceivers) Wait() <-chan ShutdownSignal { return recv.b.Wait() } ================================================ FILE: signal_test.go ================================================ // Copyright (c) 2022 Uber Technologies, Inc. // // 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. package fx import ( "context" "os" "syscall" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func assertUnsentSignalError( t *testing.T, err error, expected *unsentSignalError, ) { t.Helper() actual := new(unsentSignalError) assert.ErrorContains(t, err, "channels are blocked") if assert.ErrorAs(t, err, &actual, "is unsentSignalError") { assert.Equal(t, expected, actual) } } func TestSignal(t *testing.T) { t.Parallel() t.Run("Done", func(t *testing.T) { recv := newSignalReceivers() a := recv.Done() _ = recv.Done() // we never listen on this expected := ShutdownSignal{ Signal: syscall.SIGTERM, } require.NoError(t, recv.b.Broadcast(expected), "first broadcast should succeed") assertUnsentSignalError(t, recv.b.Broadcast(expected), &unsentSignalError{ Signal: expected, Total: 2, Unsent: 2, }) assert.Equal(t, expected.Signal, <-a) assert.Equal(t, expected.Signal, <-recv.Done(), "expect cached signal") }) t.Run("signal notify relayer", func(t *testing.T) { t.Parallel() t.Run("start and stop", func(t *testing.T) { t.Parallel() t.Run("timeout", func(t *testing.T) { recv := newSignalReceivers() recv.Start() timeoutCtx, cancel := context.WithTimeout(context.Background(), 0) defer cancel() err := recv.Stop(timeoutCtx) require.ErrorIs(t, err, context.DeadlineExceeded) }) t.Run("no error", func(t *testing.T) { recv := newSignalReceivers() ctx := t.Context() recv.Start() recv.Start() // should be a no-op if already running require.NoError(t, recv.Stop(ctx)) }) t.Run("notify", func(t *testing.T) { stub := make(chan os.Signal) recv := newSignalReceivers() recv.notify = func(ch chan<- os.Signal, _ ...os.Signal) { go func() { for sig := range stub { ch <- sig } }() } var stopCalledTimes int recv.stopNotify = func(ch chan<- os.Signal) { stopCalledTimes++ } ctx := t.Context() recv.Start() stub <- syscall.SIGTERM stub <- syscall.SIGTERM require.Equal(t, syscall.SIGTERM, <-recv.Done()) require.Equal(t, syscall.SIGTERM, <-recv.Done()) sig := <-recv.Wait() require.Equal(t, syscall.SIGTERM, sig.Signal) require.NoError(t, recv.Stop(ctx)) require.Equal(t, 1, stopCalledTimes) close(stub) }) }) }) t.Run("stop deadlock", func(t *testing.T) { recv := newSignalReceivers() var notify chan<- os.Signal recv.notify = func(ch chan<- os.Signal, _ ...os.Signal) { notify = ch } recv.Start() // Artificially create a race where the relayer receives an OS signal // while Stop() holds the lock. If this leads to deadlock, // we will receive a context timeout error. gotErr := make(chan error, 1) notify <- syscall.SIGTERM go func() { stopCtx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() gotErr <- recv.Stop(stopCtx) }() assert.NoError(t, <-gotErr) }) } ================================================ FILE: supply.go ================================================ // Copyright (c) 2020 Uber Technologies, Inc. // // 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. package fx import ( "fmt" "reflect" "strings" "go.uber.org/fx/internal/fxreflect" ) // Supply provides instantiated values for dependency injection as if // they had been provided using a constructor that simply returns them. // The most specific type of each value (as determined by reflection) is used. // // This serves a purpose similar to what fx.Replace does for fx.Decorate. // // For example, given: // // type ( // TypeA struct{} // TypeB struct{} // TypeC struct{} // ) // // var a, b, c = &TypeA{}, TypeB{}, &TypeC{} // // The following two forms are equivalent: // // fx.Supply(a, b, fx.Annotated{Target: c}) // // fx.Provide( // func() *TypeA { return a }, // func() TypeB { return b }, // fx.Annotated{Target: func() *TypeC { return c }}, // ) // // Supply panics if a value (or annotation target) is an untyped nil or an error. // // [Private] can be used to restrict access to supplied values. // // # Supply Caveats // // As mentioned above, Supply uses the most specific type of the provided // value. For interface values, this refers to the type of the implementation, // not the interface. So if you supply an http.Handler, fx.Supply will use the // type of the implementation. // // var handler http.Handler = http.HandlerFunc(f) // fx.Supply(handler) // // Is equivalent to, // // fx.Provide(func() http.HandlerFunc { return f }) // // This is typically NOT what you intended. To supply the handler above as an // http.Handler, we need to use the fx.Annotate function with the fx.As // annotation. // // fx.Supply( // fx.Annotate(handler, fx.As(new(http.Handler))), // ) func Supply(values ...any) Option { constructors := make([]any, 0, len(values)) types := make([]reflect.Type, 0, len(values)) var private bool for _, value := range values { var ( typ reflect.Type ctor any ) switch value := value.(type) { case privateOption: private = true continue case annotated: value.Target, typ = newSupplyConstructor(value.Target) ctor = value case Annotated: value.Target, typ = newSupplyConstructor(value.Target) ctor = value default: ctor, typ = newSupplyConstructor(value) } constructors = append(constructors, ctor) types = append(types, typ) } return supplyOption{ Targets: constructors, Types: types, Stack: fxreflect.CallerStack(1, 0), Private: private, } } type supplyOption struct { Targets []any Types []reflect.Type // type of value produced by constructor[i] Stack fxreflect.Stack Private bool } func (o supplyOption) apply(m *module) { for i, target := range o.Targets { m.provides = append(m.provides, provide{ Target: target, Stack: o.Stack, IsSupply: true, SupplyType: o.Types[i], Private: o.Private, }) } } func (o supplyOption) String() string { items := make([]string, 0, len(o.Targets)) for _, typ := range o.Types { items = append(items, typ.String()) } return fmt.Sprintf("fx.Supply(%s)", strings.Join(items, ", ")) } // Returns a function that takes no parameters, and returns the given value. func newSupplyConstructor(value any) (any, reflect.Type) { switch value.(type) { case nil: panic("untyped nil passed to fx.Supply") case error: panic("error value passed to fx.Supply") } typ := reflect.TypeOf(value) returnTypes := []reflect.Type{typ} returnValues := []reflect.Value{reflect.ValueOf(value)} ft := reflect.FuncOf([]reflect.Type{}, returnTypes, false) fv := reflect.MakeFunc(ft, func([]reflect.Value) []reflect.Value { return returnValues }) return fv.Interface(), typ } ================================================ FILE: supply_test.go ================================================ // Copyright (c) 2020 Uber Technologies, Inc. // // 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. package fx_test import ( "bytes" "errors" "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "go.uber.org/fx/fxevent" "go.uber.org/fx/fxtest" "go.uber.org/fx/internal/fxlog" ) func TestSupply(t *testing.T) { t.Parallel() type A struct{} type B struct{} t.Run("NothingIsSupplied", func(t *testing.T) { t.Parallel() app := fxtest.New(t, fx.Supply()) defer app.RequireStart().RequireStop() }) t.Run("SomethingIsSupplied", func(t *testing.T) { t.Parallel() aIn, bIn := &A{}, &B{} var aOut *A var bOut *B app := fxtest.New( t, fx.Supply(aIn, bIn), fx.Populate(&aOut, &bOut), ) defer app.RequireStart().RequireStop() require.Same(t, aIn, aOut) require.Same(t, bIn, bOut) }) t.Run("SupplyInModule", func(t *testing.T) { t.Parallel() aIn, bIn := &A{}, &B{} var aOut *A var bOut *B app := fxtest.New( t, fx.Module("module", fx.Supply(aIn, bIn), ), fx.Populate(&aOut, &bOut), ) defer app.RequireStart().RequireStop() require.Same(t, aIn, aOut) require.Same(t, bIn, bOut) }) t.Run("AnnotateIsSupplied", func(t *testing.T) { t.Parallel() firstIn, secondIn, thirdIn := &A{}, &A{}, &B{} var out struct { fx.In First *A `name:"first"` Second *A `name:"second"` Third *B } app := fxtest.New( t, fx.Supply( fx.Annotated{Name: "first", Target: firstIn}, fx.Annotated{Name: "second", Target: secondIn}, thirdIn), fx.Populate(&out), ) defer app.RequireStart().RequireStop() require.Same(t, firstIn, out.First) require.Same(t, secondIn, out.Second) require.Same(t, thirdIn, out.Third) }) t.Run("AnnotateIsSupported", func(t *testing.T) { t.Parallel() var out struct { fx.In Got io.Writer } var give bytes.Buffer app := fxtest.New(t, fx.Supply(fx.Annotate(&give, fx.As(new(io.Writer)))), fx.Populate(&out), ) defer app.RequireStart().RequireStop() require.Same(t, &give, out.Got) }) t.Run("InvalidArgumentIsSupplied", func(t *testing.T) { t.Parallel() require.PanicsWithValuef( t, "untyped nil passed to fx.Supply", func() { fx.Supply(A{}, nil) }, "a naked nil should panic", ) require.NotPanicsf( t, func() { fx.Supply(A{}, (*B)(nil)) }, "a wrapped nil should not panic") require.PanicsWithValuef( t, "error value passed to fx.Supply", func() { fx.Supply(A{}, errors.New("fail")) }, "an error value should panic", ) }) t.Run("SupplyCollision", func(t *testing.T) { t.Parallel() type foo struct{} var spy fxlog.Spy app := fx.New( fx.WithLogger(func() fxevent.Logger { return &spy }), fx.Supply(&foo{}, &foo{}), ) require.Error(t, app.Err()) assert.Contains(t, app.Err().Error(), "already provided") supplied := spy.Events().SelectByTypeName("Supplied") require.Len(t, supplied, 2) require.NoError(t, supplied[0].(*fxevent.Supplied).Err) require.Error(t, supplied[1].(*fxevent.Supplied).Err) }) t.Run("SupplyToASoftGroup", func(t *testing.T) { t.Parallel() type Param struct { fx.In Foos []string `group:"foo,soft"` Bar []int `group:"bar"` } type Result struct { fx.Out Foo string `group:"foo"` Bar int `group:"bar"` } app := fxtest.New(t, fx.Supply( Result{ Foo: "sad", Bar: 20, }), fx.Supply( fx.Annotated{ Target: 10, Group: "bar", }, fx.Annotated{ Target: "bye", Group: "foo", }), fx.Supply(fx.Annotated{ Target: "hello", Group: "foo", }), fx.Invoke(func(p Param) { assert.ElementsMatch(t, []string{"sad"}, p.Foos) }), ) defer app.RequireStart().RequireStop() }) } ================================================ FILE: tools/analysis/passes/allfxevents/analysis.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. // Package allfxevents implements a Go analysis pass that verifies that an // fxevent.Logger implementation handles all known fxevent types. As a special // case for no-op or fake fxevent.Loggers, it ignores implementations that // handle none of the event types. // // This is meant for use within Fx only. package allfxevents import ( "fmt" "go/ast" "go/token" "go/types" "sort" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" ) // Analyzer is a go/analysis compatible analyzer that verifies that all // fxevent.Loggers shipped with Fx handle all known Fx event types. var Analyzer = &analysis.Analyzer{ Name: "allfxevents", Doc: "check for unhandled fxevent.Events", Run: run, Requires: []*analysis.Analyzer{ inspect.Analyzer, }, } var _filter = []ast.Node{ &ast.File{}, &ast.FuncDecl{}, &ast.CaseClause{}, &ast.TypeAssertExpr{}, } func run(pass *analysis.Pass) (interface{}, error) { fxeventPkg, ok := findPackage(pass.Pkg, "go.uber.org/fx/fxevent") if !ok { // If the package doesn't import fxevent, and itself isn't // fxevent, then we don't need to run this pass. return nil, nil } v := visitor{ Fxevent: inspectFxevent(fxeventPkg), Fset: pass.Fset, Info: pass.TypesInfo, Report: pass.Report, } pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes(_filter, v.Visit) return nil, nil } type visitor struct { Fset *token.FileSet Info *types.Info Fxevent fxevent Report func(analysis.Diagnostic) // types not yet referenced by this function loggerType types.Type funcEvents *typeSet } func (v *visitor) Visit(n ast.Node, push bool) (recurse bool) { switch n := n.(type) { case *ast.File: if !push { return false } // Don't run the linter on test files. fname := v.Fset.File(n.Pos()).Name() return !strings.HasSuffix(fname, "_test.go") case *ast.FuncDecl: if !push { return v.funcDeclExit(n) } return v.funcDeclEnter(n) case *ast.CaseClause: if !push { return false } for _, expr := range n.List { t := v.Info.Types[expr].Type if t != nil { v.funcEvents.Remove(t) } } case *ast.TypeAssertExpr: if !push { return false } t := v.Info.Types[n.Type].Type if t != nil { v.funcEvents.Remove(t) } } return false } func (v *visitor) funcDeclEnter(n *ast.FuncDecl) bool { // Skip top-level functions, and methods not named // LogEvent. if n.Recv == nil || n.Name.Name != "LogEvent" { return false } // Skip types that don't implement fxevent.Logger. t := v.Info.Types[n.Recv.List[0].Type].Type if t == nil || !types.Implements(t, v.Fxevent.LoggerInterface) { return false } // Each function declaration gets its own copy of the typeSet to track // events in. v.loggerType = t v.funcEvents = v.Fxevent.Events.Clone() return true } func (v *visitor) funcDeclExit(n *ast.FuncDecl) bool { nEvents := v.funcEvents.Len() if nEvents == 0 { return false } // If the logger doesn't handle *any* event type, it's probably a fake, // or a no-op implementation. Don't bother with it. if nEvents == v.Fxevent.Events.Len() { return false } missing := make([]string, 0, nEvents) v.funcEvents.Iterate(func(t types.Type) { // Use a fxevent qualifier so that event names don't include // the full import path of the fxevent package. missing = append(missing, types.TypeString(t, emptyQualifier)) }) sort.Strings(missing) v.Report(analysis.Diagnostic{ Pos: n.Pos(), Message: fmt.Sprintf("%v doesn't handle %v", types.TypeString(v.loggerType, emptyQualifier), missing, ), }) return false } // Find the package with the given import path. func findPackage(pkg *types.Package, importPath string) (_ *types.Package, ok bool) { if pkg.Path() == importPath { return pkg, true } for _, imp := range pkg.Imports() { if imp.Path() == importPath { return imp, true } } return nil, false } // fxevent holds type information extracted from the fxevent package necessary // for inspection. type fxevent struct { Logger types.Type // fxevent.Logger LoggerInterface *types.Interface // raw type information for fxevent.Logger Event types.Type // fxevent.Type Events typeSet } func inspectFxevent(pkg *types.Package) fxevent { scope := pkg.Scope() event := scope.Lookup("Event").Type() var eventTypes typeSet for _, name := range scope.Names() { if name == "Event" { continue } obj := scope.Lookup(name) if !obj.Exported() { continue } typ := obj.Type() if !types.ConvertibleTo(typ, event) { typ = types.NewPointer(typ) if !types.ConvertibleTo(typ, event) { continue } } eventTypes.Put(typ) } logger := scope.Lookup("Logger").Type() return fxevent{ Logger: logger, LoggerInterface: logger.Underlying().(*types.Interface), Event: event, Events: eventTypes, } } // A set of types.Type objects. The zero value is valid. type typeSet struct{ m typeutil.Map } func (ts *typeSet) Len() int { return ts.m.Len() } // Put puts an item into the set. func (ts *typeSet) Put(t types.Type) { ts.m.Set(t, struct{}{}) } // Remove removes an item from the set, reporting whether it was found in the // set. func (ts *typeSet) Remove(t types.Type) (found bool) { return ts.m.Delete(t) } // Iterate iterates through the type set in an unspecified order. func (ts *typeSet) Iterate(f func(types.Type)) { ts.m.Iterate(func(t types.Type, _ interface{}) { f(t) }) } func (ts *typeSet) Clone() *typeSet { var out typeSet ts.Iterate(out.Put) return &out } // Use this as a types.Qualifier to print the name of an entity with // types.TypeString or similar without including their full package path. func emptyQualifier(*types.Package) string { return "" } ================================================ FILE: tools/analysis/passes/allfxevents/analysis_test.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. package allfxevents import ( "testing" "golang.org/x/tools/go/analysis/analysistest" ) func TestAnalyzer(t *testing.T) { t.Parallel() analysistest.Run(t, analysistest.TestData(), Analyzer, "./...") } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/a/full.go ================================================ package a import ( "fmt" "go.uber.org/fx/fxevent" ) type fullLogger struct{} func (*fullLogger) LogEvent(ev fxevent.Event) { switch ev.(type) { case *fxevent.Foo, *fxevent.Bar, *fxevent.Baz: fmt.Println(ev) case *fxevent.Qux: fmt.Println(ev) } } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/a/nop.go ================================================ package a import "go.uber.org/fx/fxevent" type nopLogger struct{} func (nopLogger) LogEvent(fxevent.Event) { // Don't do anything with the event. Should not cause a // diagnostic. } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/a/not_a_logger.go ================================================ package a import ( "fmt" "go.uber.org/fx/fxevent" ) type notALogger struct{} // Doesn't implement fxevent.Logger because it returns an error. This shouldn't // cause a diagnostic. func (*notALogger) LogEvent(ev fxevent.Event) error { _, ok := ev.(*fxevent.Foo) fmt.Println(ok) return nil } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/a/partial_test.go ================================================ package a import ( "fmt" "go.uber.org/fx/fxevent" ) // This logger intentionally doesn't handle everything. We don't expect any // diagnostics reported for it because it's in a test file. type partialLogger struct{} func (partialLogger) LogEvent(ev fxevent.Event) { _, ok := ev.(*fxevent.Foo) fmt.Println(ok) } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/a/ptr.go ================================================ package a import ( "log" "go.uber.org/fx/fxevent" ) type ptrLogger struct{} func (*ptrLogger) LogEvent(ev fxevent.Event) { // want `\*ptrLogger doesn't handle \[\*Bar \*Foo\]` if e, ok := ev.(*fxevent.Baz); ok { log.Print(e) } if e, ok := ev.(*fxevent.Qux); ok { log.Print(e) } } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/a/value.go ================================================ package a import ( "fmt" "io" "go.uber.org/fx/fxevent" ) type valueLogger struct { W io.Writer } func (l valueLogger) LogEvent(ev fxevent.Event) { // want `valueLogger doesn't handle \[\*Baz \*Qux\]` switch ev.(type) { case *fxevent.Foo: fmt.Fprintln(l.W, ev) case *fxevent.Bar: fmt.Fprintln(l.W, ev) } } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/b/fxevent/logger.go ================================================ package fxevent type ( Event struct{} Logger interface{ LogEvent(Event) } ) ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/b/not_real_fxevent.go ================================================ package b import ( "b/fxevent" "fmt" ) type Logger struct{} var _ fxevent.Logger = Logger{} func (Logger) LogEvent(ev fxevent.Event) { fmt.Println(ev) } ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/go.uber.org/fx/fxevent/fxevent.go ================================================ package fxevent // This is a partial fxevent package inspired by the real fxevent package, // but with a fixed list of events we can test against. type ( Logger interface{ LogEvent(Event) } Event interface{ event() } Foo struct{} Bar struct{} Baz struct{} Qux struct{} ) func (*Foo) event() {} func (*Bar) event() {} func (*Baz) event() {} func (*Qux) event() {} ================================================ FILE: tools/analysis/passes/allfxevents/testdata/src/go.uber.org/fx/fxevent/partial.go ================================================ package fxevent // Partial logger implementation in the same package as fxevent.Logger. type partialLogger struct{} func (partialLogger) LogEvent(ev Event) { // want `partialLogger doesn't handle \[\*Qux\]` switch ev.(type) { case *Foo: case *Bar: case *Baz: } } ================================================ FILE: tools/cmd/fxlint/main.go ================================================ // Copyright (c) 2021 Uber Technologies, Inc. // // 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. // fxlint is a linter that verifies correct usage of Fx. // // Currently, the following passes are provided: // // - allfxevents: Verifies that all Fx events are handled by an fxevent.Logger. package main import ( "go.uber.org/fx/tools/analysis/passes/allfxevents" "golang.org/x/tools/go/analysis/multichecker" ) func main() { multichecker.Main( allfxevents.Analyzer, ) } ================================================ FILE: tools/go.mod ================================================ module go.uber.org/fx/tools go 1.22.0 toolchain go1.24.0 require golang.org/x/tools v0.30.0 require ( golang.org/x/mod v0.23.0 // indirect golang.org/x/sync v0.11.0 // indirect ) ================================================ FILE: tools/go.sum ================================================ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= ================================================ FILE: version.go ================================================ // Copyright (c) 2019 Uber Technologies, Inc. // // 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. package fx // Version is exported for runtime compatibility checks. const Version = "1.25.0-dev"