Compare commits

..

No commits in common. "main" and "2.0.0" have entirely different histories.
main ... 2.0.0

41 changed files with 1649 additions and 3198 deletions

View File

@ -1,8 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

14
.github/release.yml vendored
View File

@ -1,14 +0,0 @@
changelog:
categories:
- title: SemVer Major
labels:
- ⚠️ semver/major
- title: SemVer Minor
labels:
- 🆕 semver/minor
- title: SemVer Patch
labels:
- 🔨 semver/patch
- title: Other Changes
labels:
- semver/none

View File

@ -1,26 +0,0 @@
name: Main
on:
push:
branches: [main]
schedule:
- cron: "0 8,20 * * *"
jobs:
unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
macos-tests:
name: macOS tests
uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
with:
runner_pool: nightly
build_scheme: swift-metrics-Package

View File

@ -1,34 +0,0 @@
name: PR
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
soundness:
name: Soundness
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
with:
license_header_check_project_name: "Swift Metrics API"
unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_1_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
cxx-interop:
name: Cxx interop
uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
macos-tests:
name: macOS tests
uses: apple/swift-nio/.github/workflows/macos_tests.yml@main
with:
runner_pool: general
build_scheme: swift-metrics-Package

View File

@ -1,18 +0,0 @@
name: PR label
on:
pull_request:
types: [labeled, unlabeled, opened, reopened, synchronize]
jobs:
semver-label-check:
name: Semantic version label check
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Check for Semantic Version label
uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main

1
.gitignore vendored
View File

@ -6,4 +6,3 @@
.SourceKitten
*.orig
.swiftpm
Package.resolved

View File

@ -1,35 +0,0 @@
.gitignore
**/.gitignore
.licenseignore
.gitattributes
.git-blame-ignore-revs
.mailfilter
.mailmap
.spi.yml
.swift-format
.editorconfig
.github/*
*.md
*.txt
*.yml
*.yaml
*.json
Package.swift
**/Package.swift
Package@-*.swift
**/Package@-*.swift
Package.resolved
**/Package.resolved
Makefile
*.modulemap
**/*.modulemap
**/*.docc/*
*.xcprivacy
**/*.xcprivacy
*.symlink
**/*.symlink
Dockerfile
**/Dockerfile
Snippets/*
dev/git.commit.template
.unacceptablelanguageignore

View File

@ -1,4 +0,0 @@
version: 1
builder:
configs:
- documentation_targets: [CoreMetrics, Metrics, MetricsTestKit]

View File

@ -1,68 +0,0 @@
{
"version" : 1,
"indentation" : {
"spaces" : 4
},
"tabWidth" : 4,
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"spacesAroundRangeFormationOperators" : false,
"indentConditionalCompilationBlocks" : false,
"indentSwitchCaseLabels" : false,
"lineBreakAroundMultilineExpressionChainComponents" : false,
"lineBreakBeforeControlFlowKeywords" : false,
"lineBreakBeforeEachArgument" : true,
"lineBreakBeforeEachGenericRequirement" : true,
"lineLength" : 120,
"maximumBlankLines" : 1,
"respectsExistingLineBreaks" : true,
"prioritizeKeepingFunctionOutputTogether" : true,
"noAssignmentInExpressions" : {
"allowedFunctions" : [
"XCTAssertNoThrow",
"XCTAssertThrowsError"
]
},
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLiteralForEmptyCollectionInit" : false,
"AlwaysUseLowerCamelCase" : false,
"AmbiguousTrailingClosureOverload" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : true,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : false,
"NeverUseForceTry" : false,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : true,
"NoAssignmentInExpressions" : true,
"NoBlockComments" : true,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : true,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OmitExplicitReturns" : true,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : true,
"OrderedImports" : true,
"ReplaceForEachWithForLoop" : true,
"ReturnVoidInsteadOfEmptyTuple" : true,
"UseEarlyExits" : false,
"UseExplicitNilCheckInConditions" : false,
"UseLetInEveryBoundCaseVariable" : false,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : false,
"UseSynthesizedInitializer" : false,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
}
}

13
.swiftformat Normal file
View File

@ -0,0 +1,13 @@
# file options
--exclude .build
# format options
--self insert
--patternlet inline
--stripunusedargs unnamed-only
--comments ignore
--ifdef no-indent
# rules

View File

@ -1,3 +1,55 @@
# Code of Conduct
To be a truly great community, SwiftMetrics needs to welcome developers from all walks of life,
with different backgrounds, and with a wide range of experience. A diverse and friendly
community will have more great ideas, more unique perspectives, and produce more great
code. We will work diligently to make the SwiftMetrics community welcoming to everyone.
The code of conduct for this project can be found at https://swift.org/code-of-conduct.
To give clarity of what is expected of our members, SwiftMetrics has adopted the code of conduct
defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source
communities, and we think it articulates our values well. The full text is copied below:
### Contributor Code of Conduct v1.3
As contributors and maintainers of this project, and in the interest of fostering an open and
welcoming community, we pledge to respect all people who contribute through reporting
issues, posting feature requests, updating documentation, submitting pull requests or patches,
and other activities.
We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression, sexual
orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or
nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing others private information, such as physical or electronic addresses, without explicit permission
- Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or reject comments,
commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of
Conduct, or to ban temporarily or permanently any contributor for other behaviors that they
deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
consistently applying these principles to every aspect of managing this project. Project
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
from the project team.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting a project maintainer at [swift-server-conduct@group.apple.com](mailto:swift-server-conduct@group.apple.com). All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to the
circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter
of an incident.
*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).*
### Reporting
A working group of community members is committed to promptly addressing any [reported issues](mailto:swift-server-conduct@group.apple.com).
Working group members are volunteers appointed by the project lead, with a
preference for individuals with varied backgrounds and perspectives. Membership is expected
to change regularly, and may grow or shrink.

View File

@ -60,9 +60,10 @@ We require that your commit messages match our template. The easiest way to do t
git config commit.template dev/git.commit.template
### Run CI checks locally
### Make sure Tests work on Linux
You can run the Github Actions workflows locally using [act](https://github.com/nektos/act). For detailed steps on how to do this please see [https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally](https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally).
SwiftMetrics uses XCTest to run tests on both macOS and Linux. While the macOS version of XCTest is able to use the Objective-C runtime to discover tests at execution time, the Linux version is not.
For this reason, whenever you add new tests **you have to run a script** that generates the hooks needed to run those tests on Linux, or our CI will complain that the tests are not all present on Linux. To do this, merely execute `ruby ./scripts/generate_linux_tests.rb` at the root of the package and check the changes it made.
## How to contribute your work

View File

@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:4.2
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
@ -20,45 +20,19 @@ let package = Package(
products: [
.library(name: "CoreMetrics", targets: ["CoreMetrics"]),
.library(name: "Metrics", targets: ["Metrics"]),
.library(name: "MetricsTestKit", targets: ["MetricsTestKit"]),
],
targets: [
.target(
name: "CoreMetrics"
name: "CoreMetrics",
dependencies: []
),
.target(
name: "Metrics",
dependencies: ["CoreMetrics"]
),
.target(
name: "MetricsTestKit",
dependencies: ["Metrics"]
),
.testTarget(
name: "MetricsTests",
dependencies: ["Metrics", "MetricsTestKit"]
dependencies: ["Metrics"]
),
]
)
for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(.enableExperimentalFeature("StrictConcurrency=complete"))
target.swiftSettings = settings
}
// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
for target in package.targets {
switch target.type {
case .regular, .test, .executable:
var settings = target.swiftSettings ?? []
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
target.swiftSettings = settings
case .macro, .plugin, .system, .binary:
() // not applicable
@unknown default:
() // we don't know what to do here, do nothing
}
}
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //

125
README.md
View File

@ -15,20 +15,15 @@ If you have a server-side Swift application, or maybe a cross-platform (e.g. Lin
To add a dependency on the metrics API package, you need to declare it in your `Package.swift`:
```swift
// swift-metrics 1.x and 2.x are almost API compatible, so most clients should use
.package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"),
// As of May 5, 2019, SwiftMetrics' major stable release is 1.0.0
// To depend on this release, you can use
.package(url: "https://github.com/apple/swift-metrics.git", from: "1.0.0"),
```
and to your application/library target, add "Metrics" to your dependencies:
```swift
.target(
name: "BestExampleApp",
dependencies: [
// ...
.product(name: "Metrics", package: "swift-metrics"),
]
),
.target(name: "BestExampleApp", dependencies: ["Metrics"]),
```
### Emitting metrics information
@ -62,13 +57,8 @@ As the API has just launched, not many implementations exist yet. If you are int
- [SwiftPrometheus](https://github.com/MrLotU/SwiftPrometheus), support for [Prometheus](https://prometheus.io)
- [StatsD Client](https://github.com/apple/swift-statsd-client), support for StatsD
- [OpenTelemetry Swift](https://github.com/open-telemetry/opentelemetry-swift), support for [OpenTelemetry](https://opentelemetry.io/) which also implements other metrics and tracing backends
- Your library? [Get in touch!](https://forums.swift.org/c/server)
### Swift Metrics Extras
You may also be interested in some "extra" modules which are collected in the [Swift Metrics Extras](https://github.com/apple/swift-metrics-extras) repository.
## Detailed design
### Architecture
@ -90,7 +80,7 @@ This API was designed with the contributors to the Swift on Server community and
### Metric types
The API supports six metric types:
The API supports four metric types:
`Counter`: A counter is a cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart. For example, you can use a counter to represent the number of requests served, tasks completed, or errors.
@ -98,30 +88,18 @@ The API supports six metric types:
counter.increment(by: 100)
```
- `FloatingPointCounter`: A variation of a `Counter` that records a floating point value, instead of an integer.
```swift
floatingPointCounter.increment(by: 10.5)
```
`Gauge`: A Gauge is a metric that represents a single numerical value that can arbitrarily go up and down. Gauges are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads. Gauges are modeled as a `Recorder` with a sample size of 1 that does not perform any aggregation.
```swift
gauge.record(100)
```
`Meter`: A Meter is similar to `Gauge` - a metric that represents a single numerical value that can arbitrarily go up and down. Meters are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads. Unlike `Gauge`, `Meter` also supports atomic incerements and decerements.
```swift
meter.record(100)
```
`Recorder`: A recorder collects observations within a time window (usually things like response sizes) and *can* provide aggregated information about the data sample, for example count, sum, min, max and various quantiles.
```swift
recorder.record(100)
```
`Gauge`: A Gauge is a metric that represents a single numerical value that can arbitrarily go up and down. Gauges are typically used for measured values like temperatures or current memory usage, but also "counts" that can go up and down, like the number of active threads. Gauges are modeled as a `Recorder` with a sample size of 1 that does not perform any aggregation.
```swift
gauge.record(100)
```
`Timer`: A timer collects observations within a time window (usually things like request duration) and provides aggregated information about the data sample, for example min, max and various quantiles. It is similar to a `Recorder` but specialized for values that represent durations.
```swift
@ -132,7 +110,7 @@ timer.recordMilliseconds(100)
Note: Unless you need to implement a custom metrics backend, everything in this section is likely not relevant, so please feel free to skip.
As seen above, each constructor for `Counter`, `Gauge`, `Meter`, `Recorder` and `Timer` provides a metric object. This uncertainty obscures the selected metrics backend calling these constructors by design. _Each application_ can select and configure its desired backend. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:
As seen above, each constructor for `Counter`, `Timer`, `Recorder` and `Gauge` provides a metric object. This uncertainty obscures the selected metrics backend calling these constructors by design. _Each application_ can select and configure its desired backend. The application sets up the metrics backend it wishes to use. Configuring the metrics backend is straightforward:
```swift
let metricsImplementation = MyFavoriteMetricsImplementation()
@ -146,12 +124,10 @@ Given the above, an implementation of a metric backend needs to conform to `prot
```swift
public protocol MetricsFactory {
func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler
func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler
func destroyCounter(_ handler: CounterHandler)
func destroyMeter(_ handler: MeterHandler)
func destroyRecorder(_ handler: RecorderHandler)
func destroyTimer(_ handler: TimerHandler)
}
@ -168,14 +144,11 @@ public protocol CounterHandler: AnyObject {
}
```
**Meter**
**Timer**
```swift
public protocol MeterHandler: AnyObject {
func set(_ value: Int64)
func set(_ value: Double)
func increment(by: Double)
func decrement(by: Double)
public protocol TimerHandler: AnyObject {
func recordNanoseconds(_ duration: Int64)
}
```
@ -188,17 +161,9 @@ public protocol RecorderHandler: AnyObject {
}
```
**Timer**
```swift
public protocol TimerHandler: AnyObject {
func recordNanoseconds(_ duration: Int64)
}
```
#### Dealing with Overflows
Implementation of metric objects that deal with integers, like `Counter` and `Timer` should be careful with overflow. The expected behavior is to cap at `.max`, and never crash the program due to overflow . For example:
Implementaton of metric objects that deal with integers, like `Counter` and `Timer` should be careful with overflow. The expected behavior is to cap at `.max`, and never crash the program due to overflow . For example:
```swift
class ExampleCounter: CounterHandler {
@ -226,12 +191,9 @@ class SimpleMetricsLibrary: MetricsFactory {
return ExampleCounter(label, dimensions)
}
func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
return ExampleMeter(label, dimensions)
}
func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
return ExampleRecorder(label, dimensions, aggregate)
let maker: (String, [(String, String)]) -> RecorderHandler = aggregate ? ExampleRecorder.init : ExampleGauge.init
return maker(label, dimensions)
}
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
@ -240,8 +202,7 @@ class SimpleMetricsLibrary: MetricsFactory {
// implementation is stateless, so nothing to do on destroy calls
func destroyCounter(_ handler: CounterHandler) {}
func destroyMeter(_ handler: TimerHandler) {}
func destroyRecorder(_ handler: RecorderHandler) {}
func destroyRecorder(_ handler: RecorderHandler) {}
func destroyTimer(_ handler: TimerHandler) {}
private class ExampleCounter: CounterHandler {
@ -262,31 +223,8 @@ class SimpleMetricsLibrary: MetricsFactory {
}
}
private class ExampleMeter: MeterHandler {
init(_: String, _: [(String, String)]) {}
let lock = NSLock()
var _value: Double = 0
func set(_ value: Int64) {
self.set(Double(value))
}
func set(_ value: Double) {
self.lock.withLock { _value = value }
}
func increment(by value: Double) {
self.lock.withLock { self._value += value }
}
func decrement(by value: Double) {
self.lock.withLock { self._value -= value }
}
}
private class ExampleRecorder: RecorderHandler {
init(_: String, _: [(String, String)], _: Bool) {}
init(_: String, _: [(String, String)]) {}
private let lock = NSLock()
var values = [(Int64, Double)]()
@ -326,23 +264,26 @@ class SimpleMetricsLibrary: MetricsFactory {
}
}
private class ExampleTimer: TimerHandler {
private class ExampleGauge: RecorderHandler {
init(_: String, _: [(String, String)]) {}
let lock = NSLock()
var _value: Int64 = 0
var _value: Double = 0
func record(_ value: Int64) {
self.record(Double(value))
}
func record(_ value: Double) {
self.lock.withLock { _value = value }
}
}
private class ExampleTimer: ExampleRecorder, TimerHandler {
func recordNanoseconds(_ duration: Int64) {
self.lock.withLock { _value = duration }
super.record(duration)
}
}
}
```
## Security
Please see [SECURITY.md](SECURITY.md) for details on the security process.
## Getting involved
Do not hesitate to get in touch as well, over on https://forums.swift.org/c/server

View File

@ -1,32 +0,0 @@
# Release process
Swift metrics follows a relatively simple release process, outlined below.
### Issue and PR management basics
The overall goal of Issue and Pull Request management is to allow referring to them in the future, and being able to
consistently and easily find out in which release a certain issue was fixed.
- most work should have an associated GitHub issue, and a Pull Request resolving it should mark it so by e.g. using github's "resolves #1234" mechanism or otherwise make it clear which issue it resolves.
- when a PR is merged, the associated issue is closed and the issue is assigned to the milestone the change is going to be released in.
- if a pull request was made directly, and has no associated issue, the pull request is associated with the milestone instead.
- do not assign both an issue _and_ pull request about the same ticket to the same milestone as it may be confusing why a similar sounding issue was "solved twice".
### Release process
Once it is decided that a release should be cut, follow these steps to make sure the release is nice and clean.
In our example let's consider we're cutting a release for the version `1.2.3`.
- check all outstanding PRs, if any can be merged right away for this release, consider doing so,
- make sure all recently closed PRs or issues have been assigned to the milestone (assign them to the milestone `1.2.3` if not already done),
- create the "next" release milestone, for example `1.2.4` (or `1.3.0` if necessary) and move remaining issues to is,
- this way these tickets are carried over to the "next" release and are a bit easier to find and prioritize.
- close the current milestone (`1.2.3`),
- pull and tag the current commit with `1.2.3`
- prefer signing your tag (`git tag -s`) so it can be confirmed who performed the release and the tag is trustworthy,
- push the tag,
- update and upload the documentation,
- e.g. use jazzy to generate and push the documentation branch (TODO: more details here).
- finally, go to the GitHub releases page and [draft a new release](https://github.com/apple/swift-metrics/releases/new).

View File

@ -1,43 +0,0 @@
# Security
This document specifies the security process for the SwiftMetrics project.
## Disclosures
### Private Disclosure Process
The SwiftMetrics maintainers ask that known and suspected vulnerabilities be
privately and responsibly disclosed by emailing
[sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org)
with the all the required detail.
**Do not file a public issue.**
#### When to report a vulnerability
* You think you have discovered a potential security vulnerability in SwiftMetrics.
* You are unsure how a vulnerability affects SwiftMetrics.
#### What happens next?
* A member of the team will acknowledge receipt of the report within 3
working days (United States). This may include a request for additional
information about reproducing the vulnerability.
* We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the
vulnerability within 10 days of the report as per their [security
guidelines][sswg-security].
* Once we have identified a fix we may ask you to validate it. We aim to do this
within 30 days. In some cases this may not be possible, for example when the
vulnerability exists at the protocol level and the industry must coordinate on
the disclosure process.
* If a CVE number is required, one will be requested from [MITRE][mitre]
providing you with full credit for the discovery.
* We will decide on a planned release date and let you know when it is.
* Prior to release, we will inform major dependents that a security-related
patch is impending.
* Once the fix has been released we will publish a security advisory on GitHub
and in the Server → Security Updates category on the [Swift forums][swift-forums-sec].
[sswg]: https://github.com/swift-server/sswg
[sswg-security]: https://www.swift.org/sswg/security/
[swift-forums-sec]: https://forums.swift.org/c/server/security-updates/
[mitre]: https://cveform.mitre.org/

View File

@ -1,99 +0,0 @@
# ``CoreMetrics``
A Metrics API package for Swift.
## Overview
Almost all production server software needs to emit metrics information for observability. Because it's unlikely that all parties can agree on one specific metrics backend implementation, this API is designed to establish a standard that can be implemented by various metrics libraries which then post the metrics data to backends like [Prometheus](https://prometheus.io/), [Graphite](https://graphiteapp.org), publish over [statsd](https://github.com/statsd/statsd), write to disk, etc.
This is the beginning of a community-driven open-source project actively seeking contributions, be it code, documentation, or ideas. Apart from contributing to SwiftMetrics itself, we need metrics compatible libraries which send the metrics over to backend such as the ones mentioned above. What SwiftMetrics provides today is covered in the [API docs](https://apple.github.io/swift-metrics/), but it will continue to evolve with community input.
## Getting started
If you have a server-side Swift application, or maybe a cross-platform (e.g. Linux, macOS) application or library, and you would like to emit metrics, targeting this metrics API package is a great idea. Below you'll find all you need to know to get started.
### Adding the dependency
To add a dependency on the metrics API package, you need to declare it in your `Package.swift`:
```swift
// swift-metrics 1.x and 2.x are almost API compatible, so most clients should use
.package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"),
```
and to your application/library target, add "Metrics" to your dependencies:
```swift
.target(
name: "BestExampleApp",
dependencies: [
// ...
.product(name: "Metrics", package: "swift-metrics"),
]
),
```
### Emitting metrics information
```swift
// 1) let's import the metrics API package
import Metrics
// 2) we need to create a concrete metric object, the label works similarly to a `DispatchQueue` label
let counter = Counter(label: "com.example.BestExampleApp.numberOfRequests")
// 3) we're now ready to use it
counter.increment()
```
### Selecting a metrics backend implementation (applications only)
Note: If you are building a library, you don't need to concern yourself with this section. It is the end users of your library (the applications) who will decide which metrics backend to use. Libraries should never change the metrics implementation as that is something owned by the application.
SwiftMetrics only provides the metrics system API. As an application owner, you need to select a metrics backend (such as the ones mentioned above) to make the metrics information useful.
Selecting a backend is done by adding a dependency on the desired backend client implementation and invoking the `MetricsSystem.bootstrap` function at the beginning of the program:
```swift
MetricsSystem.bootstrap(SelectedMetricsImplementation())
```
This instructs the `MetricsSystem` to install `SelectedMetricsImplementation` (actual name will differ) as the metrics backend to use.
> Tip: Refer to the project's [README](https://github.com/apple/swift-metrics) for an up-to-date list of backend implementations.
### Swift Metrics Extras
You may also be interested in some "extra" modules which are collected in the [Swift Metrics Extras](https://github.com/apple/swift-metrics-extras) repository.
## Detailed design
### Architecture
We believe that for the Swift on Server ecosystem, it's crucial to have a metrics API that can be adopted by anybody so a multitude of libraries from different parties can all provide metrics information. More concretely this means that we believe all the metrics events from all libraries should end up in the same place, be one of the backends mentioned above or wherever else the application owner may choose.
In the real world, there are so many opinions over how exactly a metrics system should behave, how metrics should be aggregated and calculated, and where/how to persist them. We think it's not feasible to wait for one metrics package to support everything that a specific deployment needs while still being simple enough to use and remain performant. That's why we decided to split the problem into two:
1. a metrics API
2. a metrics backend implementation
This package only provides the metrics API itself, and therefore, SwiftMetrics is a "metrics API package." SwiftMetrics can be configured (using `MetricsSystem.bootstrap`) to choose any compatible metrics backend implementation. This way, packages can adopt the API, and the application can choose any compatible metrics backend implementation without requiring any changes from any of the libraries.
This API was designed with the contributors to the Swift on Server community and approved by the SSWG (Swift Server Work Group) to the "sandbox level" of the SSWG's incubation process.
[pitch](https://forums.swift.org/t/metrics/19353) |
[discussion](https://forums.swift.org/t/discussion-server-metrics-api/) |
[feedback](https://forums.swift.org/t/feedback-server-metrics-api/)
## Topics
### Metric types
- ``Counter``
- ``FloatingPointCounter``
- ``Meter``
- ``Recorder``
- ``Gauge``
- ``Timer``

View File

@ -26,58 +26,29 @@
//
//===----------------------------------------------------------------------===//
#if canImport(WASILibc)
// No locking on WASILibc
#elseif canImport(Darwin)
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
import Darwin
#elseif os(Windows)
import WinSDK
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Android)
import Android
#elseif canImport(Musl)
import Musl
#else
#error("Unsupported runtime")
import Glibc
#endif
/// A threading lock based on `libpthread` instead of `libdispatch`.
///
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
/// of lock is safe to use with `libpthread`-based threading models, such as the
/// one used by NIO. On Windows, the lock is based on the substantially similar
/// `SRWLOCK` type.
/// one used by NIO.
internal final class Lock {
#if os(Windows)
fileprivate let mutex: UnsafeMutablePointer<SRWLOCK> =
UnsafeMutablePointer.allocate(capacity: 1)
#else
fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> =
UnsafeMutablePointer.allocate(capacity: 1)
#endif
fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> = UnsafeMutablePointer.allocate(capacity: 1)
/// Create a new lock.
public init() {
#if os(Windows)
InitializeSRWLock(self.mutex)
#else
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
let err = pthread_mutex_init(self.mutex, &attr)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
let err = pthread_mutex_init(self.mutex, nil)
precondition(err == 0, "pthread_mutex_init failed with error \(err)")
}
deinit {
#if os(Windows)
// SRWLOCK does not need to be free'd
#else
let err = pthread_mutex_destroy(self.mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
precondition(err == 0, "pthread_mutex_destroy failed with error \(err)")
self.mutex.deallocate()
}
@ -86,12 +57,8 @@ internal final class Lock {
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
public func lock() {
#if os(Windows)
AcquireSRWLockExclusive(self.mutex)
#else
let err = pthread_mutex_lock(self.mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
precondition(err == 0, "pthread_mutex_lock failed with error \(err)")
}
/// Release the lock.
@ -99,12 +66,8 @@ internal final class Lock {
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
public func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(self.mutex)
#else
let err = pthread_mutex_unlock(self.mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
precondition(err == 0, "pthread_mutex_unlock failed with error \(err)")
}
}
@ -118,7 +81,7 @@ extension Lock {
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
func withLock<T>(_ body: () throws -> T) rethrows -> T {
internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
self.lock()
defer {
self.unlock()
@ -128,119 +91,70 @@ extension Lock {
// specialise Void return (for performance)
@inlinable
func withLockVoid(_ body: () throws -> Void) rethrows {
internal func withLockVoid(_ body: () throws -> Void) rethrows {
try self.withLock(body)
}
}
extension Lock: @unchecked Sendable {}
/// A reader/writer threading lock based on `libpthread` instead of `libdispatch`.
/// A threading lock based on `libpthread` instead of `libdispatch`.
///
/// This object provides a lock on top of a single `pthread_rwlock_t`. This kind
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
/// of lock is safe to use with `libpthread`-based threading models, such as the
/// one used by NIO. On Windows, the lock is based on the substantially similar
/// `SRWLOCK` type.
/// one used by NIO.
internal final class ReadWriteLock {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
fileprivate let rwlock: UnsafeMutablePointer<SRWLOCK> =
UnsafeMutablePointer.allocate(capacity: 1)
fileprivate var shared: Bool = true
#else
fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> =
UnsafeMutablePointer.allocate(capacity: 1)
#endif
fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> = UnsafeMutablePointer.allocate(capacity: 1)
/// Create a new lock.
public init() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
InitializeSRWLock(self.rwlock)
#else
let err = pthread_rwlock_init(self.rwlock, nil)
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
precondition(err == 0, "pthread_rwlock_init failed with error \(err)")
}
deinit {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
// SRWLOCK does not need to be free'd
#else
let err = pthread_rwlock_destroy(self.rwlock)
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
precondition(err == 0, "pthread_rwlock_destroy failed with error \(err)")
self.rwlock.deallocate()
}
/// Acquire a reader lock.
///
/// Whenever possible, consider using `withReaderLock` instead of this
/// method and `unlock`, to simplify lock handling.
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
public func lockRead() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
AcquireSRWLockShared(self.rwlock)
self.shared = true
#else
let err = pthread_rwlock_rdlock(self.rwlock)
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
precondition(err == 0, "pthread_rwlock_rdlock failed with error \(err)")
}
/// Acquire a writer lock.
///
/// Whenever possible, consider using `withWriterLock` instead of this
/// method and `unlock`, to simplify lock handling.
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
public func lockWrite() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
AcquireSRWLockExclusive(self.rwlock)
self.shared = false
#else
let err = pthread_rwlock_wrlock(self.rwlock)
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
precondition(err == 0, "pthread_rwlock_wrlock failed with error \(err)")
}
/// Release the lock.
///
/// Whenever possible, consider using `withReaderLock` and `withWriterLock`
/// instead of this method and `lockRead` and `lockWrite`, to simplify lock
/// handling.
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
public func unlock() {
#if canImport(WASILibc)
// WASILibc is single threaded, provides no locks
#elseif os(Windows)
if self.shared {
ReleaseSRWLockShared(self.rwlock)
} else {
ReleaseSRWLockExclusive(self.rwlock)
}
#else
let err = pthread_rwlock_unlock(self.rwlock)
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
#endif
precondition(err == 0, "pthread_rwlock_unlock failed with error \(err)")
}
}
extension ReadWriteLock {
/// Acquire the reader lock for the duration of the given block.
///
/// This convenience method should be preferred to `lockRead` and `unlock`
/// in most situations, as it ensures that the lock will be released
/// regardless of how `body` exits.
/// This convenience method should be preferred to `lock` and `unlock` in
/// most situations, as it ensures that the lock will be released regardless
/// of how `body` exits.
///
/// - Parameter body: The block to execute while holding the reader lock.
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockRead()
defer {
self.unlock()
@ -250,14 +164,14 @@ extension ReadWriteLock {
/// Acquire the writer lock for the duration of the given block.
///
/// This convenience method should be preferred to `lockWrite` and `unlock`
/// in most situations, as it ensures that the lock will be released
/// regardless of how `body` exits.
/// This convenience method should be preferred to `lock` and `unlock` in
/// most situations, as it ensures that the lock will be released regardless
/// of how `body` exits.
///
/// - Parameter body: The block to execute while holding the writer lock.
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
self.lockWrite()
defer {
self.unlock()
@ -267,15 +181,13 @@ extension ReadWriteLock {
// specialise Void return (for performance)
@inlinable
func withReaderLockVoid(_ body: () throws -> Void) rethrows {
internal func withReaderLockVoid(_ body: () throws -> Void) rethrows {
try self.withReaderLock(body)
}
// specialise Void return (for performance)
@inlinable
func withWriterLockVoid(_ body: () throws -> Void) rethrows {
internal func withWriterLockVoid(_ body: () throws -> Void) rethrows {
try self.withWriterLock(body)
}
}
extension ReadWriteLock: @unchecked Sendable {}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
# ``Metrics``
A Metrics API package for Swift.
Refer to `CoreMetrics` module documentation for the majority of types.

View File

@ -11,16 +11,12 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
// swift-format-ignore-file
// Note: Whitespace changes are used to workaround compiler bug
// https://github.com/swiftlang/swift/issues/79285
@_exported import CoreMetrics
@_exported import class CoreMetrics.Timer
import Foundation
@_exported import class CoreMetrics.Timer
extension Timer {
public extension Timer {
/// Convenience for measuring duration of a closure.
///
/// - parameters:
@ -28,11 +24,7 @@ extension Timer {
/// - dimensions: The dimensions for the Timer.
/// - body: Closure to run & record.
@inlinable
public static func measure<T>(
label: String,
dimensions: [(String, String)] = [],
body: @escaping () throws -> T
) rethrows -> T {
static func measure<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T {
let timer = Timer(label: label, dimensions: dimensions)
let start = DispatchTime.now().uptimeNanoseconds
defer {
@ -41,24 +33,15 @@ extension Timer {
}
return try body()
}
/// Record the time interval (with nanosecond precision) between the passed `since` dispatch time and `end` dispatch time.
///
/// - parameters:
/// - since: Start of the interval as `DispatchTime`.
/// - end: End of the interval, defaulting to `.now()`.
public func recordInterval(since: DispatchTime, end: DispatchTime = .now()) {
self.recordNanoseconds(end.uptimeNanoseconds - since.uptimeNanoseconds)
}
}
extension Timer {
public extension Timer {
/// Convenience for recording a duration based on TimeInterval.
///
/// - parameters:
/// - duration: The duration to record.
@inlinable
public func record(_ duration: TimeInterval) {
func record(_ duration: TimeInterval) {
self.recordSeconds(duration)
}
@ -67,17 +50,7 @@ extension Timer {
/// - parameters:
/// - duration: The duration to record.
@inlinable
public func record(_ duration: DispatchTimeInterval) {
// This wrapping in a optional is a workaround because DispatchTimeInterval
// is a non-frozen public enum and Dispatch is built with library evolution
// mode turned on.
// This means we should have an `@unknown default` case, but this breaks
// on non-Darwin platforms.
// Switching over an optional means that the `.none` case will map to
// `default` (which means we'll always have a valid case to go into
// the default case), but in reality this case will never exist as this
// optional will never be nil.
let duration = Optional(duration)
func record(_ duration: DispatchTimeInterval) {
switch duration {
case .nanoseconds(let value):
self.recordNanoseconds(value)
@ -89,72 +62,6 @@ extension Timer {
self.recordSeconds(value)
case .never:
self.record(0)
default:
self.record(0)
}
}
}
extension Timer {
/// Convenience for recording a duration based on `Duration`.
///
/// `Duration` will be converted to an `Int64` number of nanoseconds, and then recorded with nanosecond precision.
///
/// - Parameters:
/// - duration: The `Duration` to record.
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
@inlinable
public func record(duration: Duration) {
// `Duration` doesn't have a nice way to convert it nanoseconds or seconds,
// and manual conversion can overflow.
let seconds = duration.components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
guard !seconds.overflow else { return self.recordNanoseconds(Int64.max) }
let nanoseconds = seconds.partialValue.addingReportingOverflow(duration.components.attoseconds / 1_000_000_000)
guard !nanoseconds.overflow else { return self.recordNanoseconds(Int64.max) }
self.recordNanoseconds(nanoseconds.partialValue)
}
#if compiler(>=6.0)
/// Convenience for measuring duration of a closure.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
// DO NOT FIX THE WHITESPACE IN THE NEXT LINE UNTIL 5.10 IS UNSUPPORTED
// https://github.com/swiftlang/swift/issues/79285
body: () throws(Failure) -> Result) throws(Failure) -> Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try body()
}
/// Convenience for measuring duration of a closure.
///
/// - Parameters:
/// - clock: The clock used for measuring the duration. Defaults to the continuous clock.
/// - isolation: The isolation of the method. Defaults to the isolation of the caller.
/// - body: The closure to record the duration of.
@inlinable
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
public func measure<Result, Failure: Error, Clock: _Concurrency.Clock>(
clock: Clock = .continuous,
isolation: isolated (any Actor)? = #isolation,
// DO NOT FIX THE WHITESPACE IN THE NEXT LINE UNTIL 5.10 IS UNSUPPORTED
// https://github.com/swiftlang/swift/issues/79285
body: () async throws(Failure) -> sending Result) async throws(Failure) -> sending Result where Clock.Duration == Duration {
let start = clock.now
defer {
self.record(duration: start.duration(to: clock.now))
}
return try await body()
}
#endif
}

View File

@ -1,46 +0,0 @@
# ``MetricsTestKit``
A set of tools for testing Metrics emitting libraries.
## Overview
This module offers a ``TestMetrics`` type which can be used to bootstrap the metrics system and then assert metric values on it.
## Example
```swift
import XCTest
import Metrics
import MetricsTestKit
final class ExampleTests: XCTestCase {
var metrics: TestMetrics! = TestMetrics()
override func setUp() {
MetricsSystem.bootstrapInternal(self.metrics)
}
override func tearDown() async throws {
self.metrics = nil
MetricsSystem.bootstrapInternal(NOOPMetricsHandler.instance)
}
func test_example() async throws {
// Create a metric using the bootstrapped test metrics backend:
Recorder(label: "example").record(100)
// extract the `TestRecorder` out of the
let recorder = try self.metrics.expectRecorder("example")
recorder.lastValue?.shouldEqual(6)
}
}
```
## Topics
### Test metrics
- ``TestCounter``
- ``TestMeter``
- ``TestRecorder``
- ``TestTimer``

View File

@ -1,591 +0,0 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2021 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Cluster Membership open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift Cluster Membership project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.md for the list of Swift Cluster Membership project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import CoreMetrics
import Metrics
import XCTest
/// Taken directly from `swift-cluster-memberships`'s own test target package, which
/// adopts the `TestMetrics` from `swift-metrics`.
///
/// Metrics factory which allows inspecting recorded metrics programmatically.
/// Only intended for tests of the Metrics API itself.
///
/// Created Handlers will store Metrics until they are explicitly destroyed.
///
public final class TestMetrics: MetricsFactory {
private let lock = NSLock()
public typealias Label = String
public typealias Dimensions = String
public struct FullKey: Sendable {
let label: Label
let dimensions: [(String, String)]
}
private var _counters = [FullKey: TestCounter]()
private var _meters = [FullKey: TestMeter]()
private var _recorders = [FullKey: TestRecorder]()
private var _timers = [FullKey: TestTimer]()
public init() {
// nothing to do
}
/// Reset method to destroy all created ``TestCounter``, ``TestMeter``, ``TestRecorder`` and ``TestTimer``.
/// Invoke this method in between test runs to verify that Counters are created as needed.
public func reset() {
self.lock.withLock {
self._counters = [:]
self._recorders = [:]
self._meters = [:]
self._timers = [:]
}
}
public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
self.lock.withLock { () -> CounterHandler in
if let existing = self._counters[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestCounter(label: label, dimensions: dimensions)
self._counters[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler {
self.lock.withLock { () -> MeterHandler in
if let existing = self._meters[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestMeter(label: label, dimensions: dimensions)
self._meters[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
self.lock.withLock { () -> RecorderHandler in
if let existing = self._recorders[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
self._recorders[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
self.lock.withLock { () -> TimerHandler in
if let existing = self._timers[.init(label: label, dimensions: dimensions)] {
return existing
}
let item = TestTimer(label: label, dimensions: dimensions)
self._timers[.init(label: label, dimensions: dimensions)] = item
return item
}
}
public func destroyCounter(_ handler: CounterHandler) {
if let testCounter = handler as? TestCounter {
self.lock.withLock {
self._counters.removeValue(forKey: testCounter.key)
}
}
}
public func destroyMeter(_ handler: MeterHandler) {
if let testMeter = handler as? TestMeter {
self.lock.withLock { () in
self._meters.removeValue(forKey: testMeter.key)
}
}
}
public func destroyRecorder(_ handler: RecorderHandler) {
if let testRecorder = handler as? TestRecorder {
self.lock.withLock {
self._recorders.removeValue(forKey: testRecorder.key)
}
}
}
public func destroyTimer(_ handler: TimerHandler) {
if let testTimer = handler as? TestTimer {
self.lock.withLock {
self._timers.removeValue(forKey: testTimer.key)
}
}
}
}
extension TestMetrics.FullKey: Hashable {
public func hash(into hasher: inout Hasher) {
self.label.hash(into: &hasher)
for dim in self.dimensions {
dim.0.hash(into: &hasher)
dim.1.hash(into: &hasher)
}
}
public static func == (lhs: TestMetrics.FullKey, rhs: TestMetrics.FullKey) -> Bool {
lhs.label == rhs.label
&& Dictionary(uniqueKeysWithValues: lhs.dimensions) == Dictionary(uniqueKeysWithValues: rhs.dimensions)
}
}
// MARK: - Assertions
extension TestMetrics {
// MARK: - Counter
public func expectCounter(_ metric: Counter) throws -> TestCounter {
guard let counter = metric._handler as? TestCounter else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestCounter.self)")
}
return counter
}
public func expectCounter(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestCounter {
let maybeItem = self.lock.withLock {
self._counters[.init(label: label, dimensions: dimensions)]
}
guard let testCounter = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testCounter
}
/// All the counters which have been created and not destroyed
public var counters: [TestCounter] {
let counters = self.lock.withLock {
self._counters
}
return Array(counters.values)
}
// MARK: - Gauge
public func expectGauge(_ metric: Gauge) throws -> TestRecorder {
try self.expectRecorder(metric)
}
public func expectGauge(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestRecorder {
try self.expectRecorder(label, dimensions)
}
// MARK: - Meter
public func expectMeter(_ metric: Meter) throws -> TestMeter {
guard let meter = metric._handler as? TestMeter else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestMeter.self)")
}
return meter
}
public func expectMeter(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestMeter {
let maybeItem = self.lock.withLock {
self._meters[.init(label: label, dimensions: dimensions)]
}
guard let testMeter = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testMeter
}
/// All the meters which have been created and not destroyed
public var meters: [TestMeter] {
let meters = self.lock.withLock {
self._meters
}
return Array(meters.values)
}
// MARK: - Recorder
public func expectRecorder(_ metric: Recorder) throws -> TestRecorder {
guard let recorder = metric._handler as? TestRecorder else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestRecorder.self)")
}
return recorder
}
public func expectRecorder(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestRecorder {
let maybeItem = self.lock.withLock {
self._recorders[.init(label: label, dimensions: dimensions)]
}
guard let testRecorder = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testRecorder
}
/// All the recorders which have been created and not destroyed
public var recorders: [TestRecorder] {
let recorders = self.lock.withLock {
self._recorders
}
return Array(recorders.values)
}
// MARK: - Timer
public func expectTimer(_ metric: CoreMetrics.Timer) throws -> TestTimer {
guard let timer = metric._handler as? TestTimer else {
throw TestMetricsError.illegalMetricType(metric: metric._handler, expected: "\(TestTimer.self)")
}
return timer
}
public func expectTimer(_ label: String, _ dimensions: [(String, String)] = []) throws -> TestTimer {
let maybeItem = self.lock.withLock {
self._timers[.init(label: label, dimensions: dimensions)]
}
guard let testTimer = maybeItem else {
throw TestMetricsError.missingMetric(label: label, dimensions: dimensions)
}
return testTimer
}
/// All the timers which have been created and not destroyed
public var timers: [TestTimer] {
let timers = self.lock.withLock {
self._timers
}
return Array(timers.values)
}
}
// MARK: - Metric type implementations
public protocol TestMetric {
associatedtype Value
var key: TestMetrics.FullKey { get }
var lastValue: Value? { get }
var last: (Date, Value)? { get }
}
public final class TestCounter: TestMetric, CounterHandler, Equatable {
public let id: String
public let label: String
public let dimensions: [(String, String)]
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = UUID().uuidString
self.label = label
self.dimensions = dimensions
}
public func increment(by amount: Int64) {
self.lock.withLock {
self._values.append((Date(), amount))
}
}
public func reset() {
self.lock.withLock {
self._values = []
}
}
public var lastValue: Int64? {
self.last?.1
}
public var totalValue: Int64 {
self.values.reduce(0, +)
}
public var last: (Date, Int64)? {
self.lock.withLock {
self._values.last
}
}
public var values: [Int64] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public static func == (lhs: TestCounter, rhs: TestCounter) -> Bool {
lhs.id == rhs.id
}
}
public final class TestMeter: TestMetric, MeterHandler, Equatable {
public let id: String
public let label: String
public let dimensions: [(String, String)]
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Double)]()
init(label: String, dimensions: [(String, String)]) {
self.id = UUID().uuidString
self.label = label
self.dimensions = dimensions
}
public func set(_ value: Int64) {
self.set(Double(value))
}
public func set(_ value: Double) {
self.lock.withLock {
// this may lose precision but good enough as an example
_values.append((Date(), Double(value)))
}
}
public func increment(by amount: Double) {
// Drop illegal values
// - cannot increment by NaN
guard !amount.isNaN else {
return
}
// - cannot increment by infinite quantities
guard !amount.isInfinite else {
return
}
// - cannot increment by negative values
guard amount.sign == .plus else {
return
}
// - cannot increment by zero
guard !amount.isZero else {
return
}
self.lock.withLock {
let lastValue: Double = self._values.last?.1 ?? 0
let newValue = lastValue + amount
_values.append((Date(), newValue))
}
}
public func decrement(by amount: Double) {
// Drop illegal values
// - cannot decrement by NaN
guard !amount.isNaN else {
return
}
// - cannot decrement by infinite quantities
guard !amount.isInfinite else {
return
}
// - cannot decrement by negative values
guard amount.sign == .plus else {
return
}
// - cannot decrement by zero
guard !amount.isZero else {
return
}
self.lock.withLock {
let lastValue: Double = self._values.last?.1 ?? 0
let newValue = lastValue - amount
_values.append((Date(), newValue))
}
}
public var lastValue: Double? {
self.last?.1
}
public var last: (Date, Double)? {
self.lock.withLock {
self._values.last
}
}
public var values: [Double] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public static func == (lhs: TestMeter, rhs: TestMeter) -> Bool {
lhs.id == rhs.id
}
}
public final class TestRecorder: TestMetric, RecorderHandler, Equatable {
public let id: String
public let label: String
public let dimensions: [(String, String)]
public let aggregate: Bool
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Double)]()
init(label: String, dimensions: [(String, String)], aggregate: Bool) {
self.id = UUID().uuidString
self.label = label
self.dimensions = dimensions
self.aggregate = aggregate
}
public func record(_ value: Int64) {
self.record(Double(value))
}
public func record(_ value: Double) {
self.lock.withLock {
// this may lose precision but good enough as an example
_values.append((Date(), Double(value)))
}
}
public var lastValue: Double? {
self.last?.1
}
public var last: (Date, Double)? {
self.lock.withLock {
self._values.last
}
}
public var values: [Double] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public static func == (lhs: TestRecorder, rhs: TestRecorder) -> Bool {
lhs.id == rhs.id
}
}
public final class TestTimer: TestMetric, TimerHandler, Equatable {
public let id: String
public let label: String
public var displayUnit: TimeUnit?
public let dimensions: [(String, String)]
public var key: TestMetrics.FullKey {
TestMetrics.FullKey(label: self.label, dimensions: self.dimensions)
}
let lock = NSLock()
private var _values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = UUID().uuidString
self.label = label
self.displayUnit = nil
self.dimensions = dimensions
}
public func preferDisplayUnit(_ unit: TimeUnit) {
self.lock.withLock {
self.displayUnit = unit
}
}
public func valueInPreferredUnit(atIndex i: Int) -> Double {
let value = self.values[i]
guard let displayUnit = self.displayUnit else {
return Double(value)
}
return Double(value) / Double(displayUnit.scaleFromNanoseconds)
}
public func recordNanoseconds(_ duration: Int64) {
self.lock.withLock {
_values.append((Date(), duration))
}
}
public var lastValue: Int64? {
self.last?.1
}
public var values: [Int64] {
self.lock.withLock {
self._values.map { $0.1 }
}
}
public var last: (Date, Int64)? {
self.lock.withLock {
self._values.last
}
}
public static func == (lhs: TestTimer, rhs: TestTimer) -> Bool {
lhs.id == rhs.id
}
}
extension NSLock {
@discardableResult
fileprivate func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer {
self.unlock()
}
return body()
}
}
// MARK: - Errors
public enum TestMetricsError: Error {
case missingMetric(label: String, dimensions: [(String, String)])
case illegalMetricType(metric: Sendable, expected: String)
}
// MARK: - Sendable support
// ideally we would not be using @unchecked here, but concurrency-safety checks do not recognize locks
extension TestMetrics: @unchecked Sendable {}
extension TestCounter: @unchecked Sendable {}
extension TestMeter: @unchecked Sendable {}
extension TestRecorder: @unchecked Sendable {}
extension TestTimer: @unchecked Sendable {}

32
Tests/LinuxMain.swift Normal file
View File

@ -0,0 +1,32 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// LinuxMain.swift
//
import XCTest
///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
#if os(Linux) || os(FreeBSD)
@testable import MetricsTests
XCTMain([
testCase(MetricsExtensionsTests.allTests),
testCase(MetricsTests.allTests),
])
#endif

View File

@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// CoreMetricsTests+XCTest.swift
//
import XCTest
///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
extension MetricsTests {
static var allTests: [(String, (MetricsTests) -> () throws -> Void)] {
return [
("testCounters", testCounters),
("testCounterBlock", testCounterBlock),
("testRecorders", testRecorders),
("testRecordersInt", testRecordersInt),
("testRecordersFloat", testRecordersFloat),
("testRecorderBlock", testRecorderBlock),
("testTimers", testTimers),
("testTimerBlock", testTimerBlock),
("testTimerVariants", testTimerVariants),
("testTimerOverflow", testTimerOverflow),
("testTimerHandlesUnsignedOverflow", testTimerHandlesUnsignedOverflow),
("testGauge", testGauge),
("testGaugeBlock", testGaugeBlock),
("testMUX", testMUX),
("testCustomFactory", testCustomFactory),
("testDestroyingGauge", testDestroyingGauge),
("testDestroyingCounter", testDestroyingCounter),
("testDestroyingTimer", testDestroyingTimer),
]
}
}

View File

@ -12,10 +12,8 @@
//
//===----------------------------------------------------------------------===//
import MetricsTestKit
import XCTest
@testable import CoreMetrics
import XCTest
class MetricsTests: XCTestCase {
func testCounters() throws {
@ -23,19 +21,19 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup()
let name = "counter-\(UUID().uuidString)"
let name = "counter-\(NSUUID().uuidString)"
let counter = Counter(label: name)
let testCounter = try metrics.expectCounter(counter)
let total = Int.random(in: 500...1000)
for _ in 0..<total {
let testCounter = counter.handler as! TestCounter
let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
defer { group.leave() }
counter.increment(by: Int.random(in: 0...1000))
counter.increment(by: Int.random(in: 0 ... 1000))
group.leave()
}
}
group.wait()
XCTAssertEqual(testCounter.values.count, total, "expected number of entries to match")
XCTAssertEqual(testCounter.values.count - 1, total, "expected number of entries to match")
testCounter.reset()
XCTAssertEqual(testCounter.values.count, 0, "expected number of entries to match")
}
@ -45,126 +43,34 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "counter-\(UUID().uuidString)"
let value = Int.random(in: Int.min...Int.max)
let name = "counter-\(NSUUID().uuidString)"
let value = Int.random(in: Int.min ... Int.max)
Counter(label: name).increment(by: value)
let counter = try metrics.expectCounter(name)
let counter = metrics.counters[name] as! TestCounter
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter.values[0], Int64(value), "expected value to match")
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
counter.reset()
XCTAssertEqual(counter.values.count, 0, "expected number of entries to match")
}
func testDefaultFloatingPointCounter_ignoresNan() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: Double.nan)
fpCounter.increment(by: Double.signalingNaN)
XCTAssertEqual(counter.values.count, 0, "expected nan values to be ignored")
}
func testDefaultFloatingPointCounter_ignoresInfinity() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: Double.infinity)
fpCounter.increment(by: -Double.infinity)
XCTAssertEqual(counter.values.count, 0, "expected infinite values to be ignored")
}
func testDefaultFloatingPointCounter_ignoresNegativeValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: -100)
XCTAssertEqual(counter.values.count, 0, "expected negative values to be ignored")
}
func testDefaultFloatingPointCounter_ignoresZero() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
fpCounter.increment(by: 0)
fpCounter.increment(by: -0)
XCTAssertEqual(counter.values.count, 0, "expected zero values to be ignored")
}
func testDefaultFloatingPointCounter_ceilsExtremeValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let counter = try metrics.expectCounter(label)
// Just larger than Int64
fpCounter.increment(by: Double(sign: .plus, exponent: 63, significand: 1))
// Much larger than Int64
fpCounter.increment(by: Double.greatestFiniteMagnitude)
let values = counter.values
XCTAssertEqual(values.count, 2, "expected number of entries to match")
XCTAssertEqual(values, [Int64.max, Int64.max], "expected extremely large values to be replaced with Int64.max")
}
func testDefaultFloatingPointCounter_accumulatesFloatingPointDecimalValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let label = "\(#function)-fp-counter-\(UUID())"
let fpCounter = FloatingPointCounter(label: label)
let rawFpCounter = fpCounter._handler as! AccumulatingRoundingFloatingPointCounter
let counter = try metrics.expectCounter(label)
// Increment by a small value (perfectly representable)
fpCounter.increment(by: 0.75)
XCTAssertEqual(counter.values.count, 0, "expected number of entries to match")
// Increment by a small value that should grow the accumulated buffer past 1.0 (perfectly representable)
fpCounter.increment(by: 1.5)
var values = counter.values
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values, [2], "expected entries to match")
XCTAssertEqual(rawFpCounter.fraction, 0.25, "")
// Increment by a large value that should leave a fraction in the accumulator
// 1110506744053.76
fpCounter.increment(by: Double(sign: .plus, exponent: 40, significand: 1.01))
values = counter.values
XCTAssertEqual(values.count, 2, "expected number of entries to match")
XCTAssertEqual(values, [2, 1_110_506_744_054], "expected entries to match")
XCTAssertEqual(rawFpCounter.fraction, 0.010009765625, "expected fractional accumulated value")
}
func testRecorders() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup()
let name = "recorder-\(UUID().uuidString)"
let name = "recorder-\(NSUUID().uuidString)"
let recorder = Recorder(label: name)
let testRecorder = try metrics.expectRecorder(recorder)
let total = Int.random(in: 500...1000)
for _ in 0..<total {
let testRecorder = recorder.handler as! TestRecorder
let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
defer { group.leave() }
recorder.record(Int.random(in: Int.min...Int.max))
recorder.record(Int.random(in: Int.min ... Int.max))
group.leave()
}
}
group.wait()
XCTAssertEqual(testRecorder.values.count, total, "expected number of entries to match")
XCTAssertEqual(testRecorder.values.count - 1, total, "expected number of entries to match")
}
func testRecordersInt() throws {
@ -172,14 +78,14 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let recorder = Recorder(label: "test-recorder")
let testRecorder = try metrics.expectRecorder(recorder)
let values = (0...999).map { _ in Int32.random(in: Int32.min...Int32.max) }
for i in 0..<values.count {
let testRecorder = recorder.handler as! TestRecorder
let values = (0 ... 999).map { _ in Int32.random(in: Int32.min ... Int32.max) }
for i in 0 ... values.count - 1 {
recorder.record(values[i])
}
XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
for i in 0..<values.count {
XCTAssertEqual(Int32(testRecorder.values[i]), values[i], "expected value #\(i) to match.")
for i in 0 ... values.count - 1 {
XCTAssertEqual(Int32(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
}
}
@ -188,14 +94,14 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let recorder = Recorder(label: "test-recorder")
let testRecorder = try metrics.expectRecorder(recorder)
let values = (0...999).map { _ in Float.random(in: Float(Int32.min)...Float(Int32.max)) }
for i in 0..<values.count {
let testRecorder = recorder.handler as! TestRecorder
let values = (0 ... 999).map { _ in Float.random(in: Float(Int32.min) ... Float(Int32.max)) }
for i in 0 ... values.count - 1 {
recorder.record(values[i])
}
XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
for i in 0..<values.count {
XCTAssertEqual(Float(testRecorder.values[i]), values[i], "expected value #\(i) to match.")
for i in 0 ... values.count - 1 {
XCTAssertEqual(Float(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
}
}
@ -204,12 +110,12 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "recorder-\(UUID().uuidString)"
let value = Double.random(in: Double(Int.min)...Double(Int.max))
let name = "recorder-\(NSUUID().uuidString)"
let value = Double.random(in: Double(Int.min) ... Double(Int.max))
Recorder(label: name).record(value)
let recorder = try metrics.expectRecorder(name)
let recorder = metrics.recorders[name] as! TestRecorder
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
}
func testTimers() throws {
@ -217,19 +123,19 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let group = DispatchGroup()
let name = "timer-\(UUID().uuidString)"
let name = "timer-\(NSUUID().uuidString)"
let timer = Timer(label: name)
let testTimer = try metrics.expectTimer(timer)
let total = Int.random(in: 500...1000)
for _ in 0..<total {
let testTimer = timer.handler as! TestTimer
let total = Int.random(in: 500 ... 1000)
for _ in 0 ... total {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
defer { group.leave() }
timer.recordNanoseconds(Int64.random(in: Int64.min...Int64.max))
timer.recordNanoseconds(Int64.random(in: Int64.min ... Int64.max))
group.leave()
}
}
group.wait()
XCTAssertEqual(testTimer.values.count, total, "expected number of entries to match")
XCTAssertEqual(testTimer.values.count - 1, total, "expected number of entries to match")
}
func testTimerBlock() throws {
@ -237,12 +143,12 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: Int64.min...Int64.max)
let name = "timer-\(NSUUID().uuidString)"
let value = Int64.random(in: Int64.min ... Int64.max)
Timer(label: name).recordNanoseconds(value)
let timer = try metrics.expectTimer(name)
let timer = metrics.timers[name] as! TestTimer
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(timer.values[0], value, "expected value to match")
XCTAssertEqual(timer.values[0].1, value, "expected value to match")
}
func testTimerVariants() throws {
@ -251,27 +157,27 @@ class MetricsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = try metrics.expectTimer(timer)
let testTimer = timer.handler as! TestTimer
// nano
let nano = Int64.random(in: 0...5)
let nano = Int64.random(in: 0 ... 5)
timer.recordNanoseconds(nano)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0], nano, "expected value to match")
XCTAssertEqual(testTimer.values[0].1, nano, "expected value to match")
// micro
let micro = Int64.random(in: 0...5)
let micro = Int64.random(in: 0 ... 5)
timer.recordMicroseconds(micro)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1], micro * 1000, "expected value to match")
XCTAssertEqual(testTimer.values[1].1, micro * 1000, "expected value to match")
// milli
let milli = Int64.random(in: 0...5)
let milli = Int64.random(in: 0 ... 5)
timer.recordMilliseconds(milli)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2], milli * 1_000_000, "expected value to match")
XCTAssertEqual(testTimer.values[2].1, milli * 1_000_000, "expected value to match")
// seconds
let sec = Int64.random(in: 0...5)
let sec = Int64.random(in: 0 ... 5)
timer.recordSeconds(sec)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3], sec * 1_000_000_000, "expected value to match")
XCTAssertEqual(testTimer.values[3].1, sec * 1_000_000_000, "expected value to match")
}
func testTimerOverflow() throws {
@ -280,35 +186,35 @@ class MetricsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = try metrics.expectTimer(timer)
let testTimer = timer.handler as! TestTimer
// nano (integer)
timer.recordNanoseconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[0].1, Int64.max, "expected value to match")
// micro (integer)
timer.recordMicroseconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[1].1, Int64.max, "expected value to match")
// micro (double)
timer.recordMicroseconds(Double(Int64.max) + 1)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[1].1, Int64.max, "expected value to match")
// milli (integer)
timer.recordMilliseconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[2].1, Int64.max, "expected value to match")
// milli (double)
timer.recordMilliseconds(Double(Int64.max) + 1)
XCTAssertEqual(testTimer.values.count, 5, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[2].1, Int64.max, "expected value to match")
// seconds (integer)
timer.recordSeconds(Int64.max)
XCTAssertEqual(testTimer.values.count, 6, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[3].1, Int64.max, "expected value to match")
// seconds (double)
timer.recordSeconds(Double(Int64.max) * 1)
XCTAssertEqual(testTimer.values.count, 7, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[3].1, Int64.max, "expected value to match")
}
func testTimerHandlesUnsignedOverflow() throws {
@ -317,23 +223,23 @@ class MetricsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = try metrics.expectTimer(timer)
let testTimer = timer.handler as! TestTimer
// nano
timer.recordNanoseconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[0].1, Int64.max, "expected value to match")
// micro
timer.recordMicroseconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[1].1, Int64.max, "expected value to match")
// milli
timer.recordMilliseconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(testTimer.values[2], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[2].1, Int64.max, "expected value to match")
// seconds
timer.recordSeconds(UInt64.max)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
XCTAssertEqual(testTimer.values[3].1, Int64.max, "expected value to match")
}
func testGauge() throws {
@ -341,13 +247,13 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "gauge-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000)
let gauge = Gauge(label: name)
gauge.record(value)
let recorder = try metrics.expectRecorder(gauge)
let recorder = gauge.handler as! TestRecorder
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
}
func testGaugeBlock() throws {
@ -355,346 +261,61 @@ class MetricsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "gauge-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000)
Gauge(label: name).record(value)
let recorder = try metrics.expectRecorder(name)
let recorder = metrics.recorders[name] as! TestRecorder
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
}
func testMeter() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let meter = Meter(label: name)
meter.set(value)
let testMeter = try metrics.expectMeter(meter)
XCTAssertEqual(testMeter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeter.values[0], value, "expected value to match")
}
func testMeterBlock() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
Meter(label: name).set(value)
let testMeter = try metrics.expectMeter(name)
XCTAssertEqual(testMeter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeter.values[0], value, "expected value to match")
}
func testMeterInt() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (0...999).map { _ in Int32.random(in: Int32.min...Int32.max) }
for i in 0..<values.count {
meter.set(values[i])
}
XCTAssertEqual(values.count, testMeter.values.count, "expected number of entries to match")
for i in 0..<values.count {
XCTAssertEqual(Int32(testMeter.values[i]), values[i], "expected value #\(i) to match.")
}
}
func testMeterFloat() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (0...999).map { _ in Float.random(in: Float(Int32.min)...Float(Int32.max)) }
for i in 0..<values.count {
meter.set(values[i])
}
XCTAssertEqual(values.count, testMeter.values.count, "expected number of entries to match")
for i in 0..<values.count {
XCTAssertEqual(Float(testMeter.values[i]), values[i], "expected value #\(i) to match.")
}
}
func testMeterIncrement() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let group = DispatchGroup()
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (500...1000).map { _ in Double.random(in: 0...Double(Int32.max)) }
for i in 0..<values.count {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
defer { group.leave() }
meter.increment(by: values[i])
}
}
group.wait()
XCTAssertEqual(testMeter.values.count, values.count, "expected number of entries to match")
XCTAssertEqual(testMeter.values.last!, values.reduce(0.0, +), accuracy: 0.1, "expected total value to match")
}
func testMeterDecrement() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let group = DispatchGroup()
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
let values = (500...1000).map { _ in Double.random(in: 0...Double(Int32.max)) }
for i in 0..<values.count {
group.enter()
DispatchQueue(label: "\(name)-queue").async {
defer { group.leave() }
meter.decrement(by: values[i])
}
}
group.wait()
XCTAssertEqual(testMeter.values.count, values.count, "expected number of entries to match")
XCTAssertEqual(testMeter.values.last!, values.reduce(0.0, -), accuracy: 0.1, "expected total value to match")
}
func testDefaultMeterIgnoresNan() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: Double.nan)
meter.increment(by: Double.signalingNaN)
XCTAssertEqual(testMeter.values.count, 0, "expected nan values to be ignored")
}
func testDefaultMeterIgnoresInfinity() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: Double.infinity)
meter.increment(by: -Double.infinity)
XCTAssertEqual(testMeter.values.count, 0, "expected infinite values to be ignored")
}
func testDefaultMeterIgnoresNegativeValues() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: -100)
XCTAssertEqual(testMeter.values.count, 0, "expected negative values to be ignored")
}
func testDefaultMeterIgnoresZero() throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "meter-\(UUID().uuidString)"
let meter = Meter(label: name)
let testMeter = try metrics.expectMeter(meter)
meter.increment(by: 0)
meter.increment(by: -0)
XCTAssertEqual(testMeter.values.count, 0, "expected zero values to be ignored")
}
func testMUX_Counter() throws {
func testMUX() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = UUID().uuidString
let value = Int.random(in: Int.min...Int.max)
let muxCounter = Counter(label: name)
muxCounter.increment(by: value)
for factory in factories {
let counter = factory.counters.first
XCTAssertEqual(counter?.label, name, "expected label to match")
XCTAssertEqual(counter?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter?.lastValue, Int64(value), "expected value to match")
let name = NSUUID().uuidString
let value = Int.random(in: Int.min ... Int.max)
let mux = Counter(label: name)
mux.increment(by: value)
factories.forEach { factory in
let counter = factory.counters.first?.1 as! TestCounter
XCTAssertEqual(counter.label, name, "expected label to match")
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
}
muxCounter.reset()
for factory in factories {
let counter = factory.counters.first
XCTAssertEqual(counter?.values.count, 0, "expected number of entries to match")
mux.reset()
factories.forEach { factory in
let counter = factory.counters.first?.1 as! TestCounter
XCTAssertEqual(counter.values.count, 0, "expected number of entries to match")
}
}
func testMUX_Meter() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = UUID().uuidString
let value = Double.random(in: 0...1)
let muxMeter = Meter(label: name)
muxMeter.set(value)
for factory in factories {
let meter = factory.meters.first
XCTAssertEqual(meter?.label, name, "expected label to match")
XCTAssertEqual(meter?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(meter?.values[0], value, "expected value to match")
}
}
func testMUX_Recorder() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = UUID().uuidString
let value = Double.random(in: 0...1)
let muxRecorder = Recorder(label: name)
muxRecorder.record(value)
for factory in factories {
let recorder = factory.recorders.first
XCTAssertEqual(recorder?.label, name, "expected label to match")
XCTAssertEqual(recorder?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder?.values[0], value, "expected value to match")
}
}
func testMUX_Timer() throws {
// bootstrap with our test metrics
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
// run the test
let name = UUID().uuidString
let seconds = Int.random(in: 1...10)
let muxTimer = Timer(label: name, preferredDisplayUnit: .minutes)
muxTimer.recordSeconds(seconds)
for factory in factories {
let timer = factory.timers.first
XCTAssertEqual(timer?.label, name, "expected label to match")
XCTAssertEqual(timer?.values.count, 1, "expected number of entries to match")
XCTAssertEqual(timer?.values[0], Int64(seconds * 1_000_000_000), "expected value to match")
XCTAssertEqual(timer?.displayUnit, .minutes, "expected value to match")
XCTAssertEqual(
timer?.valueInPreferredUnit(atIndex: 0),
Double(seconds) / 60.0,
"seconds should be returned as minutes"
)
}
}
func testCustomHandler() {
final class CustomHandler: CounterHandler {
func testCustomFactory() {
class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {}
}
let counter1 = Counter(label: "foo")
XCTAssertFalse(counter1._handler is CustomHandler, "expected non-custom log handler")
XCTAssertFalse(counter1.handler is CustomHandler, "expected non-custom log handler")
let counter2 = Counter(label: "foo", dimensions: [], handler: CustomHandler())
XCTAssertTrue(counter2._handler is CustomHandler, "expected custom log handler")
}
func testCustomFactory() {
// @unchecked Sendable is okay here - locking is done manually.
final class CustomFactory: MetricsFactory, @unchecked Sendable {
init(handler: CustomHandler) {
self.handler = handler
}
final class CustomHandler: CounterHandler {
func increment<DataType>(by: DataType) where DataType: BinaryInteger {}
func reset() {}
}
private let handler: CustomHandler
private let lock: NSLock = NSLock()
private var locked_didCallDestroyCounter: Bool = false
var didCallDestroyCounter: Bool {
self.lock.lock()
defer {
lock.unlock()
}
return self.locked_didCallDestroyCounter
}
func makeCounter(label: String, dimensions: [(String, String)]) -> any CoreMetrics.CounterHandler {
handler
}
func makeRecorder(
label: String,
dimensions: [(String, String)],
aggregate: Bool
) -> any CoreMetrics.RecorderHandler {
fatalError("Unsupported")
}
func makeTimer(label: String, dimensions: [(String, String)]) -> any CoreMetrics.TimerHandler {
fatalError("Unsupported")
}
func destroyCounter(_ handler: any CoreMetrics.CounterHandler) {
XCTAssertTrue(
handler === self.handler,
"The handler to be destroyed doesn't match the expected handler."
)
self.lock.lock()
defer {
lock.unlock()
}
self.locked_didCallDestroyCounter = true
}
func destroyRecorder(_ handler: any CoreMetrics.RecorderHandler) {
fatalError("Unsupported")
}
func destroyTimer(_ handler: any CoreMetrics.TimerHandler) {
fatalError("Unsupported")
}
}
let handler = CustomFactory.CustomHandler()
let factory = CustomFactory(handler: handler)
XCTAssertFalse(factory.didCallDestroyCounter)
do {
let counter1 = Counter(label: "foo", factory: factory)
XCTAssertTrue(counter1._handler is CustomFactory.CustomHandler, "expected a custom metrics handler")
XCTAssertTrue(counter1._factory is CustomFactory, "expected a custom metrics factory")
counter1.destroy()
}
XCTAssertTrue(factory.didCallDestroyCounter)
XCTAssertTrue(counter2.handler is CustomHandler, "expected custom log handler")
}
func testDestroyingGauge() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "gauge-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let name = "gauge-\(NSUUID().uuidString)"
let value = Double.random(in: -1000 ... 1000)
let gauge = Gauge(label: name)
gauge.record(value)
let recorder = try metrics.expectRecorder(gauge)
let recorder = gauge.handler as! TestRecorder
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorder.values.first, value, "expected value to match")
XCTAssertEqual(recorder.values.first!.1, value, "expected value to match")
XCTAssertEqual(metrics.recorders.count, 1, "recorder should have been stored")
let identity = ObjectIdentifier(recorder)
@ -704,65 +325,27 @@ class MetricsTests: XCTestCase {
let gaugeAgain = Gauge(label: name)
gaugeAgain.record(-value)
let recorderAgain = try metrics.expectRecorder(gaugeAgain)
let recorderAgain = gaugeAgain.handler as! TestRecorder
XCTAssertEqual(recorderAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(recorderAgain.values.first, -value, "expected value to match")
XCTAssertEqual(recorderAgain.values.first!.1, -value, "expected value to match")
let identityAgain = ObjectIdentifier(recorderAgain)
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
}
func testDestroyingMeter() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let meter = Meter(label: name)
meter.set(value)
let testMeter = try metrics.expectMeter(meter)
XCTAssertEqual(testMeter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeter.values.first, value, "expected value to match")
XCTAssertEqual(metrics.meters.count, 1, "recorder should have been stored")
let identity = ObjectIdentifier(testMeter)
meter.destroy()
XCTAssertEqual(metrics.recorders.count, 0, "recorder should have been released")
let meterAgain = Meter(label: name)
meterAgain.set(-value)
let testMeterAgain = try metrics.expectMeter(meterAgain)
XCTAssertEqual(testMeterAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testMeterAgain.values.first, -value, "expected value to match")
let identityAgain = ObjectIdentifier(testMeterAgain)
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
XCTAssertNotEqual(identity, identityAgain, "since the cached metric was released, the created a new should have a different identity")
}
func testDestroyingCounter() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "counter-\(UUID().uuidString)"
let value = Int.random(in: 0...1000)
let name = "counter-\(NSUUID().uuidString)"
let value = Int.random(in: 0 ... 1000)
let counter = Counter(label: name)
counter.increment(by: value)
let testCounter = try metrics.expectCounter(counter)
let testCounter = counter.handler as! TestCounter
XCTAssertEqual(testCounter.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testCounter.values.first, Int64(value), "expected value to match")
XCTAssertEqual(testCounter.values.first!.1, Int64(value), "expected value to match")
XCTAssertEqual(metrics.counters.count, 1, "counter should have been stored")
let identity = ObjectIdentifier(counter)
@ -772,31 +355,27 @@ class MetricsTests: XCTestCase {
let counterAgain = Counter(label: name)
counterAgain.increment(by: value)
let testCounterAgain = try metrics.expectCounter(counterAgain)
let testCounterAgain = counterAgain.handler as! TestCounter
XCTAssertEqual(testCounterAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testCounterAgain.values.first, Int64(value), "expected value to match")
XCTAssertEqual(testCounterAgain.values.first!.1, Int64(value), "expected value to match")
let identityAgain = ObjectIdentifier(counterAgain)
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
XCTAssertNotEqual(identity, identityAgain, "since the cached metric was released, the created a new should have a different identity")
}
func testDestroyingTimer() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: 0...1000)
let name = "timer-\(NSUUID().uuidString)"
let value = Int64.random(in: 0 ... 1000)
let timer = Timer(label: name)
timer.recordNanoseconds(value)
let testTimer = try metrics.expectTimer(timer)
let testTimer = timer.handler as! TestTimer
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values.first, value, "expected value to match")
XCTAssertEqual(testTimer.values.first!.1, value, "expected value to match")
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
let identity = ObjectIdentifier(timer)
@ -805,35 +384,11 @@ class MetricsTests: XCTestCase {
let timerAgain = Timer(label: name)
timerAgain.recordNanoseconds(value)
let testTimerAgain = try metrics.expectTimer(timerAgain)
let testTimerAgain = timerAgain.handler as! TestTimer
XCTAssertEqual(testTimerAgain.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimerAgain.values.first, value, "expected value to match")
XCTAssertEqual(testTimerAgain.values.first!.1, value, "expected value to match")
let identityAgain = ObjectIdentifier(timerAgain)
XCTAssertNotEqual(
identity,
identityAgain,
"since the cached metric was released, the created a new should have a different identity"
)
}
func testDescriptions() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let counter = Counter(label: "hello.counter")
XCTAssertEqual("\(counter)", "Counter(hello.counter, dimensions: [])")
let gauge = Gauge(label: "hello.gauge")
XCTAssertEqual("\(gauge)", "Gauge(hello.gauge, dimensions: [], aggregate: false)")
let meter = Meter(label: "hello.meter")
XCTAssertEqual("\(meter)", "Meter(hello.meter, dimensions: [])")
let timer = Timer(label: "hello.timer")
XCTAssertEqual("\(timer)", "Timer(hello.timer, dimensions: [])")
let recorder = Recorder(label: "hello.recorder")
XCTAssertEqual("\(recorder)", "Recorder(hello.recorder, dimensions: [], aggregate: true)")
XCTAssertNotEqual(identity, identityAgain, "since the cached metric was released, the created a new should have a different identity")
}
}

View File

@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// MetricsTests+XCTest.swift
//
import XCTest
///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
extension MetricsExtensionsTests {
static var allTests: [(String, (MetricsExtensionsTests) -> () throws -> Void)] {
return [
("testTimerBlock", testTimerBlock),
("testTimerWithTimeInterval", testTimerWithTimeInterval),
("testTimerWithDispatchTime", testTimerWithDispatchTime),
("testTimerUnits", testTimerUnits),
("testPreferDisplayUnit", testPreferDisplayUnit),
]
}
}

View File

@ -12,11 +12,9 @@
//
//===----------------------------------------------------------------------===//
import MetricsTestKit
import XCTest
@testable import CoreMetrics
@testable import Metrics
import XCTest
class MetricsExtensionsTests: XCTestCase {
func testTimerBlock() throws {
@ -24,14 +22,14 @@ class MetricsExtensionsTests: XCTestCase {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let name = "timer-\(NSUUID().uuidString)"
let delay = 0.05
Timer.measure(label: name) {
Thread.sleep(forTimeInterval: delay)
}
let timer = try metrics.expectTimer(name)
let timer = metrics.timers[name] as! TestTimer
XCTAssertEqual(1, timer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(timer.values[0], Int64(delay * 1_000_000_000), "expected delay to match")
XCTAssertGreaterThan(timer.values[0].1, Int64(delay * 1_000_000_000), "expected delay to match")
}
func testTimerWithTimeInterval() throws {
@ -40,11 +38,11 @@ class MetricsExtensionsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = try metrics.expectTimer(timer)
let timeInterval = TimeInterval(Double.random(in: 1...500))
let testTimer = timer.handler as! TestTimer
let timeInterval = TimeInterval(Double.random(in: 1 ... 500))
timer.record(timeInterval)
XCTAssertEqual(1, testTimer.values.count, "expected number of entries to match")
XCTAssertEqual(testTimer.values[0], Int64(timeInterval * 1_000_000_000), "expected value to match")
XCTAssertEqual(testTimer.values[0].1, Int64(timeInterval * 1_000_000_000), "expected value to match")
}
func testTimerWithDispatchTime() throws {
@ -53,226 +51,95 @@ class MetricsExtensionsTests: XCTestCase {
MetricsSystem.bootstrapInternal(metrics)
// run the test
let timer = Timer(label: "test-timer")
let testTimer = try metrics.expectTimer(timer)
let testTimer = timer.handler as! TestTimer
// nano
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1...500))
let nano = DispatchTimeInterval.nanoseconds(Int.random(in: 1 ... 500))
timer.record(nano)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[0]), nano.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[0].1), nano.nano(), "expected value to match")
// micro
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1...500))
let micro = DispatchTimeInterval.microseconds(Int.random(in: 1 ... 500))
timer.record(micro)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[1]), micro.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[1].1), micro.nano(), "expected value to match")
// milli
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1...500))
let milli = DispatchTimeInterval.milliseconds(Int.random(in: 1 ... 500))
timer.record(milli)
XCTAssertEqual(testTimer.values.count, 3, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[2]), milli.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[2].1), milli.nano(), "expected value to match")
// seconds
let sec = DispatchTimeInterval.seconds(Int.random(in: 1...500))
let sec = DispatchTimeInterval.seconds(Int.random(in: 1 ... 500))
timer.record(sec)
XCTAssertEqual(testTimer.values.count, 4, "expected number of entries to match")
XCTAssertEqual(Int(testTimer.values[3]), sec.nano(), "expected value to match")
XCTAssertEqual(Int(testTimer.values[3].1), sec.nano(), "expected value to match")
// never
timer.record(DispatchTimeInterval.never)
XCTAssertEqual(testTimer.values.count, 5, "expected number of entries to match")
XCTAssertEqual(testTimer.values[4], 0, "expected value to match")
}
func testTimerWithDispatchTimeInterval() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(UUID().uuidString)"
let timer = Timer(label: name)
let start = DispatchTime.now()
let end = DispatchTime(uptimeNanoseconds: start.uptimeNanoseconds + 1000 * 1000 * 1000)
timer.recordInterval(since: start, end: end)
let testTimer = try metrics.expectTimer(timer)
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(
UInt64(testTimer.values.first!),
end.uptimeNanoseconds - start.uptimeNanoseconds,
"expected value to match"
)
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
}
func testTimerDuration() throws {
// Wrapping only the insides of the test case so that the generated
// tests on Linux in MetricsTests+XCTest don't complain that the func does not exist.
guard #available(iOS 16, macOS 13, tvOS 15, watchOS 8, *) else {
throw XCTSkip("Timer.record(_ duration: Duration) is not available on this platform")
}
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(UUID().uuidString)"
let timer = Timer(label: name)
let duration = Duration(secondsComponent: 3, attosecondsComponent: 123_000_000_000_000_000)
let nanoseconds = duration.components.seconds * 1_000_000_000 + duration.components.attoseconds / 1_000_000_000
timer.record(duration: duration)
// Record a Duration that would overflow,
// expect Int64.max to be recorded.
timer.record(duration: Duration(secondsComponent: 10_000_000_000, attosecondsComponent: 123))
let testTimer = try metrics.expectTimer(timer)
XCTAssertEqual(testTimer.values.count, 2, "expected number of entries to match")
XCTAssertEqual(testTimer.values.first, nanoseconds, "expected value to match")
XCTAssertEqual(testTimer.values[1], Int64.max, "expected to record Int64.max if Durataion overflows")
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
XCTAssertEqual(testTimer.values[4].1, 0, "expected value to match")
}
func testTimerUnits() throws {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: 0...1000)
let name = "timer-\(NSUUID().uuidString)"
let value = Int64.random(in: 0 ... 1000)
let timer = Timer(label: name)
timer.recordNanoseconds(value)
let testTimer = try metrics.expectTimer(timer)
let testTimer = timer.handler as! TestTimer
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(testTimer.values.first, value, "expected value to match")
XCTAssertEqual(testTimer.values.first!.1, value, "expected value to match")
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
let secondsName = "timer-seconds-\(UUID().uuidString)"
let secondsValue = Int64.random(in: 0...1000)
let secondsName = "timer-seconds-\(NSUUID().uuidString)"
let secondsValue = Int64.random(in: 0 ... 1000)
let secondsTimer = Timer(label: secondsName, preferredDisplayUnit: .seconds)
secondsTimer.recordSeconds(secondsValue)
let testSecondsTimer = try metrics.expectTimer(secondsTimer)
let testSecondsTimer = secondsTimer.handler as! TestTimer
XCTAssertEqual(testSecondsTimer.values.count, 1, "expected number of entries to match")
XCTAssertEqual(metrics.timers.count, 2, "timer should have been stored")
}
func testPreferDisplayUnit() throws {
func testPreferDisplayUnit() {
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
let value = Double.random(in: 0...1000)
let value = Double.random(in: 0 ... 1000)
let timer = Timer(label: "test", preferredDisplayUnit: .seconds)
timer.recordSeconds(value)
let testTimer = try metrics.expectTimer(timer)
let testTimer = timer.handler as! TestTimer
testTimer.preferDisplayUnit(.nanoseconds)
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value * 1000 * 1000 * 1000,
accuracy: 1.0,
"expected value to match"
)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000 * 1000, accuracy: 1.0, "expected value to match")
testTimer.preferDisplayUnit(.microseconds)
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value * 1000 * 1000,
accuracy: 0.001,
"expected value to match"
)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000, accuracy: 0.001, "expected value to match")
testTimer.preferDisplayUnit(.milliseconds)
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value * 1000,
accuracy: 0.000001,
"expected value to match"
)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000, accuracy: 0.000001, "expected value to match")
testTimer.preferDisplayUnit(.seconds)
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value,
accuracy: 0.000000001,
"expected value to match"
)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value, accuracy: 0.000000001, "expected value to match")
testTimer.preferDisplayUnit(.minutes)
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value / 60,
accuracy: 0.000000001,
"expected value to match"
)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / 60, accuracy: 0.000000001, "expected value to match")
testTimer.preferDisplayUnit(.hours)
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value / (60 * 60),
accuracy: 0.000000001,
"expected value to match"
)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60), accuracy: 0.000000001, "expected value to match")
testTimer.preferDisplayUnit(.days)
XCTAssertEqual(
testTimer.valueInPreferredUnit(atIndex: 0),
value / (60 * 60 * 24),
accuracy: 0.000000001,
"expected value to match"
)
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match")
}
#if compiler(>=6.0)
func testTimerMeasure() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}
let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}
@MainActor
func testTimerMeasureFromMainActor() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
// run the test
let name = "timer-\(UUID().uuidString)"
let delay = Duration.milliseconds(5)
let timer = Timer(label: name)
try await timer.measure {
try await Task.sleep(for: delay)
}
let expectedTimer = try metrics.expectTimer(name)
XCTAssertEqual(1, expectedTimer.values.count, "expected number of entries to match")
XCTAssertGreaterThan(expectedTimer.values[0], delay.nanosecondsClamped, "expected delay to match")
}
#endif
}
// https://bugs.swift.org/browse/SR-6310
extension DispatchTimeInterval {
func nano() -> Int {
// This wrapping in a optional is a workaround because DispatchTimeInterval
// is a non-frozen public enum and Dispatch is built with library evolution
// mode turned on.
// This means we should have an `@unknown default` case, but this breaks
// on non-Darwin platforms.
// Switching over an optional means that the `.none` case will map to
// `default` (which means we'll always have a valid case to go into
// the default case), but in reality this case will never exist as this
// optional will never be nil.
let interval = Optional(self)
switch interval {
switch self {
case .nanoseconds(let value):
return value
case .microseconds(let value):
@ -283,30 +150,6 @@ extension DispatchTimeInterval {
return value * 1_000_000_000
case .never:
return 0
default:
return 0
}
}
}
#if swift(>=5.7)
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
extension Swift.Duration {
fileprivate var nanosecondsClamped: Int64 {
let components = self.components
let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000)
let attosCompononentNanos = components.attoseconds / 1_000_000_000
let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos)
guard
!secondsComponentNanos.overflow,
!combinedNanos.overflow
else {
return .max
}
return combinedNanos.partialValue
}
}
#endif

View File

@ -0,0 +1,187 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
@testable import CoreMetrics
@testable import class CoreMetrics.Timer
import Foundation
/// Metrics factory which allows inspecting recorded metrics programmatically.
/// Only intended for tests of the Metrics API itself.
internal final class TestMetrics: MetricsFactory {
private let lock = NSLock()
var counters = [String: CounterHandler]()
var recorders = [String: RecorderHandler]()
var timers = [String: TimerHandler]()
public func makeCounter(label: String, dimensions: [(String, String)]) -> CounterHandler {
return self.make(label: label, dimensions: dimensions, registry: &self.counters, maker: TestCounter.init)
}
public func makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler {
let maker = { (label: String, dimensions: [(String, String)]) -> RecorderHandler in
TestRecorder(label: label, dimensions: dimensions, aggregate: aggregate)
}
return self.make(label: label, dimensions: dimensions, registry: &self.recorders, maker: maker)
}
public func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
return self.make(label: label, dimensions: dimensions, registry: &self.timers, maker: TestTimer.init)
}
private func make<Item>(label: String, dimensions: [(String, String)], registry: inout [String: Item], maker: (String, [(String, String)]) -> Item) -> Item {
return self.lock.withLock {
let item = maker(label, dimensions)
registry[label] = item
return item
}
}
func destroyCounter(_ handler: CounterHandler) {
if let testCounter = handler as? TestCounter {
self.counters.removeValue(forKey: testCounter.label)
}
}
func destroyRecorder(_ handler: RecorderHandler) {
if let testRecorder = handler as? TestRecorder {
self.recorders.removeValue(forKey: testRecorder.label)
}
}
func destroyTimer(_ handler: TimerHandler) {
if let testTimer = handler as? TestTimer {
self.timers.removeValue(forKey: testTimer.label)
}
}
}
internal class TestCounter: CounterHandler, Equatable {
let id: String
let label: String
let dimensions: [(String, String)]
let lock = NSLock()
var values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = NSUUID().uuidString
self.label = label
self.dimensions = dimensions
}
func increment(by amount: Int64) {
self.lock.withLock {
self.values.append((Date(), amount))
}
print("adding \(amount) to \(self.label)")
}
func reset() {
self.lock.withLock {
self.values = []
}
print("reseting \(self.label)")
}
public static func == (lhs: TestCounter, rhs: TestCounter) -> Bool {
return lhs.id == rhs.id
}
}
internal class TestRecorder: RecorderHandler, Equatable {
let id: String
let label: String
let dimensions: [(String, String)]
let aggregate: Bool
let lock = NSLock()
var values = [(Date, Double)]()
init(label: String, dimensions: [(String, String)], aggregate: Bool) {
self.id = NSUUID().uuidString
self.label = label
self.dimensions = dimensions
self.aggregate = aggregate
}
func record(_ value: Int64) {
self.record(Double(value))
}
func record(_ value: Double) {
self.lock.withLock {
// this may loose precision but good enough as an example
values.append((Date(), Double(value)))
}
print("recording \(value) in \(self.label)")
}
public static func == (lhs: TestRecorder, rhs: TestRecorder) -> Bool {
return lhs.id == rhs.id
}
}
internal class TestTimer: TimerHandler, Equatable {
let id: String
let label: String
var displayUnit: TimeUnit?
let dimensions: [(String, String)]
let lock = NSLock()
var values = [(Date, Int64)]()
init(label: String, dimensions: [(String, String)]) {
self.id = NSUUID().uuidString
self.label = label
self.displayUnit = nil
self.dimensions = dimensions
}
func preferDisplayUnit(_ unit: TimeUnit) {
self.lock.withLock {
self.displayUnit = unit
}
}
func retriveValueInPreferredUnit(atIndex i: Int) -> Double {
return self.lock.withLock {
let value = values[i].1
guard let displayUnit = self.displayUnit else {
return Double(value)
}
return Double(value) / Double(displayUnit.scaleFromNanoseconds)
}
}
func recordNanoseconds(_ duration: Int64) {
self.lock.withLock {
values.append((Date(), duration))
}
print("recording \(duration) \(self.label)")
}
public static func == (lhs: TestTimer, rhs: TestTimer) -> Bool {
return lhs.id == rhs.id
}
}
private extension NSLock {
func withLock<T>(_ body: () -> T) -> T {
self.lock()
defer {
self.unlock()
}
return body()
}
}

View File

@ -1,101 +0,0 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2022 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Dispatch
import MetricsTestKit
import XCTest
@testable import CoreMetrics
class SendableTest: XCTestCase {
func testSendableMetrics() async throws {
// bootstrap with our test metrics
let metrics = TestMetrics()
MetricsSystem.bootstrapInternal(metrics)
do {
let name = "counter-\(UUID().uuidString)"
let value = Int.random(in: 0...1000)
let counter = Counter(label: name)
let task = Task.detached { () -> [Int64] in
counter.increment(by: value)
let handler = try metrics.expectCounter(counter)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], Int64(value), "expected value to match")
}
do {
let name = "floating-point-counter-\(UUID().uuidString)"
let value = Double.random(in: 0...0.9999)
let counter = FloatingPointCounter(label: name)
let task = Task.detached { () -> Double in
counter.increment(by: value)
let handler = counter._handler as! AccumulatingRoundingFloatingPointCounter
return handler.fraction
}
let fraction = await task.value
XCTAssertEqual(fraction, value)
}
do {
let name = "recorder-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let recorder = Recorder(label: name)
let task = Task.detached { () -> [Double] in
recorder.record(value)
let handler = try metrics.expectRecorder(recorder)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], value, "expected value to match")
}
do {
let name = "meter-\(UUID().uuidString)"
let value = Double.random(in: -1000...1000)
let meter = Meter(label: name)
let task = Task.detached { () -> [Double] in
meter.set(value)
let handler = try metrics.expectMeter(meter)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], value, "expected value to match")
}
do {
let name = "timer-\(UUID().uuidString)"
let value = Int64.random(in: 0...1000)
let timer = Timer(label: name)
let task = Task.detached { () -> [Int64] in
timer.recordNanoseconds(value)
let handler = try metrics.expectTimer(timer)
return handler.values
}
let values = try await task.value
XCTAssertEqual(values.count, 1, "expected number of entries to match")
XCTAssertEqual(values[0], value, "expected value to match")
}
}
}

37
docker/Dockerfile Normal file
View File

@ -0,0 +1,37 @@
ARG swift_version=5.0
ARG ubuntu_version=bionic
# backwards compatibility for 4.2 images
ARG image_version=$swift_version-$ubuntu_version
FROM swift:$image_version
# needed to do again after FROM due to docker limitation
ARG swift_version
ARG ubuntu_version
# set as UTF-8
RUN apt-get update && apt-get install -y locales locales-all
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
# dependencies
RUN apt-get update && apt-get install -y wget
RUN apt-get update && apt-get install -y lsof dnsutils netcat-openbsd net-tools curl jq # used by integration tests
# ruby and jazzy for docs generation
RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev
RUN gem install jazzy --no-ri --no-rdoc
# tools
RUN mkdir -p $HOME/.tools
RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile
# script to allow mapping framepointers on linux (until part of the toolchain)
RUN wget -q https://raw.githubusercontent.com/apple/swift/master/utils/symbolicate-linux-fatal -O $HOME/.tools/symbolicate-linux-fatal
RUN chmod 755 $HOME/.tools/symbolicate-linux-fatal
# swiftformat (until part of the toolchain)
ARG swiftformat_version=0.40.12
RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format
RUN cd $HOME/.tools/swift-format && swift build -c release
RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat

View File

@ -0,0 +1,15 @@
version: "3"
services:
runtime-setup:
image: swift-metrics:18.04-4.2
build:
args:
image_version: "4.2"
test:
image: swift-metrics:18.04-4.2
shell:
image: swift-metrics:18.04-4.2

View File

@ -0,0 +1,16 @@
version: "3"
services:
runtime-setup:
image: swift-metrics:18.04-5.0
build:
args:
ubuntu_version: "bionic"
swift_version: "5.0"
test:
image: swift-metrics:18.04-5.0
shell:
image: swift-metrics:18.04-5.0

View File

@ -0,0 +1,18 @@
version: "3"
services:
runtime-setup:
image: swift-metrics:18.04-5.1
build:
args:
ubuntu_version: "bionic"
swift_version: "5.1"
test:
image: swift-metrics:18.04-5.1
environment: []
#- SANITIZER_ARG=--sanitize=thread
shell:
image: swift-metrics:18.04-5.1

View File

@ -0,0 +1,43 @@
# this file is not designed to be run directly
# instead, use the docker-compose.<os>.<swift> files
# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.1804.50.yaml run test
version: "3"
services:
runtime-setup:
image: swift-metrics:default
build:
context: .
dockerfile: Dockerfile
common: &common
image: swift-metrics:default
depends_on: [runtime-setup]
volumes:
- ~/.ssh:/root/.ssh
- ..:/code:z
working_dir: /code
cap_drop:
- CAP_NET_RAW
- CAP_NET_BIND_SERVICE
sanity:
<<: *common
command: /bin/bash -xcl "./scripts/sanity.sh"
docs:
<<: *common
environment:
- CI
command: /bin/bash -xcl "./scripts/generate_docs.sh"
test:
<<: *common
command: /bin/bash -xcl "swift test -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}"
# util
shell:
<<: *common
entrypoint: /bin/bash

94
scripts/build_podspec.sh Executable file
View File

@ -0,0 +1,94 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 2019 Apple Inc. and the Swift Metrics API project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
function usage() {
echo "$0 [-u] version"
echo
echo "OPTIONS:"
echo " -u: Additionally upload the podspec"
}
upload=false
while getopts ":u" opt; do
case $opt in
u)
upload=true
;;
\?)
usage
exit 1
;;
esac
done
shift "$((OPTIND-1))"
if [[ $# -eq 0 ]]; then
echo "Must provide target version"
exit 1
fi
version=$1
name="SwiftMetrics"
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
tmpdir=$(mktemp -d /tmp/.build_podspecsXXXXXX)
echo "Building podspec in $tmpdir"
cat >> "${tmpdir}/${name}.podspec" <<- EOF
Pod::Spec.new do |s|
s.name = '$name'
s.version = '$version'
s.license = { :type => 'Apache 2.0', :file => 'LICENSE.txt' }
s.summary = 'A Metrics API for Swift.'
s.homepage = 'https://github.com/apple/swift-metrics'
s.author = 'Apple Inc.'
s.source = { :git => 'https://github.com/apple/swift-metrics.git', :tag => s.version.to_s }
s.documentation_url = 'https://apple.github.io/swift-metrics'
s.module_name = '$name'
s.swift_version = '4.2'
s.cocoapods_version = '>=1.6.0'
s.ios.deployment_target = '8.0'
s.osx.deployment_target = '10.9'
s.tvos.deployment_target = '9.0'
s.watchos.deployment_target = '2.0'
s.source_files = 'Sources/CoreMetrics/**/*.swift'
end
EOF
if $upload; then
echo "Uploading ${tmpdir}/${name}.podspec"
pod trunk push "${tmpdir}/${name}.podspec"
else
echo "Linting ${tmpdir}/${name}.podspec"
pod spec lint "${tmpdir}/${name}.podspec"
fi

136
scripts/check_no_api_breakages.sh Executable file
View File

@ -0,0 +1,136 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 2019 Apple Inc. and the Swift Metrics API project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
# repodir
function all_modules() {
local repodir="$1"
(
set -eu
cd "$repodir"
swift package dump-package | jq '.products |
map(select(.type | has("library") )) |
map(.name) | .[]' | tr -d '"'
)
}
# repodir tag output
function build_and_do() {
local repodir=$1
local tag=$2
local output=$3
(
cd "$repodir"
git checkout -q "$tag"
swift build
while read -r module; do
swift api-digester -sdk "$sdk" -dump-sdk -module "$module" \
-o "$output/$module.json" -I "$repodir/.build/debug"
done < <(all_modules "$repodir")
)
}
function usage() {
echo >&2 "Usage: $0 REPO-GITHUB-URL NEW-VERSION OLD-VERSIONS..."
echo >&2
echo >&2 "This script requires a Swift 5.1+ toolchain."
echo >&2
echo >&2 "Examples:"
echo >&2
echo >&2 "Check between master and tag 2.1.1 of swift-nio:"
echo >&2 " $0 https://github.com/apple/swift-nio master 2.1.1"
echo >&2
echo >&2 "Check between HEAD and commit 64cf63d7 using the provided toolchain:"
echo >&2 " xcrun --toolchain org.swift.5120190702a $0 ../some-local-repo HEAD 64cf63d7"
}
if [[ $# -lt 3 ]]; then
usage
exit 1
fi
sdk=/
if [[ "$(uname -s)" == Darwin ]]; then
sdk=$(xcrun --show-sdk-path)
fi
hash jq 2> /dev/null || { echo >&2 "ERROR: jq must be installed"; exit 1; }
tmpdir=$(mktemp -d /tmp/.check-api_XXXXXX)
repo_url=$1
new_tag=$2
shift 2
repodir="$tmpdir/repo"
git clone "$repo_url" "$repodir"
git -C "$repodir" fetch -q origin '+refs/pull/*:refs/remotes/origin/pr/*'
errors=0
for old_tag in "$@"; do
mkdir "$tmpdir/api-old"
mkdir "$tmpdir/api-new"
echo "Checking public API breakages from $old_tag to $new_tag"
build_and_do "$repodir" "$new_tag" "$tmpdir/api-new/"
build_and_do "$repodir" "$old_tag" "$tmpdir/api-old/"
for f in "$tmpdir/api-new"/*; do
f=$(basename "$f")
report="$tmpdir/$f.report"
if [[ ! -f "$tmpdir/api-old/$f" ]]; then
echo "NOTICE: NEW MODULE $f"
continue
fi
echo -n "Checking $f... "
swift api-digester -sdk "$sdk" -diagnose-sdk \
--input-paths "$tmpdir/api-old/$f" -input-paths "$tmpdir/api-new/$f" 2>&1 \
> "$report" 2>&1
if ! shasum "$report" | grep -q cefc4ee5bb7bcdb7cb5a7747efa178dab3c794d5; then
echo ERROR
echo >&2 "=============================="
echo >&2 "ERROR: public API change in $f"
echo >&2 "=============================="
cat >&2 "$report"
errors=$(( errors + 1 ))
else
echo OK
fi
done
rm -rf "$tmpdir/api-new" "$tmpdir/api-old"
done
if [[ "$errors" == 0 ]]; then
echo "OK, all seems good"
fi
echo done
exit "$errors"

View File

@ -0,0 +1,39 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' )
cat > "$here/../CONTRIBUTORS.txt" <<- EOF
For the purpose of tracking copyright, this is the list of individuals and
organizations who have contributed source code to the Swift Metrics API.
For employees of an organization/company where the copyright of work done
by employees of that company is held by the company itself, only the company
needs to be listed here.
## COPYRIGHT HOLDERS
- Apple Inc. (all contributors with '@apple.com')
### Contributors
$contributors
**Updating this list**
Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\`
EOF

122
scripts/generate_docs.sh Executable file
View File

@ -0,0 +1,122 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -e
my_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
root_path="$my_path/.."
version=$(git describe --abbrev=0 --tags || echo "0.0.0")
modules=(CoreMetrics Metrics)
if [[ "$(uname -s)" == "Linux" ]]; then
# build code if required
if [[ ! -d "$root_path/.build/x86_64-unknown-linux" ]]; then
swift build
fi
# setup source-kitten if required
source_kitten_source_path="$root_path/.SourceKitten"
if [[ ! -d "$source_kitten_source_path" ]]; then
git clone https://github.com/jpsim/SourceKitten.git "$source_kitten_source_path"
fi
source_kitten_path="$source_kitten_source_path/.build/x86_64-unknown-linux/debug"
if [[ ! -d "$source_kitten_path" ]]; then
rm -rf "$source_kitten_source_path/.swift-version"
cd "$source_kitten_source_path" && swift build && cd "$root_path"
fi
# generate
mkdir -p "$root_path/.build/sourcekitten"
for module in "${modules[@]}"; do
if [[ ! -f "$root_path/.build/sourcekitten/$module.json" ]]; then
"$source_kitten_path/sourcekitten" doc --spm-module $module > "$root_path/.build/sourcekitten/$module.json"
fi
done
fi
[[ -d docs/$version ]] || mkdir -p docs/$version
[[ -d swift-metrics.xcodeproj ]] || swift package generate-xcodeproj
# run jazzy
if ! command -v jazzy > /dev/null; then
gem install jazzy --no-ri --no-rdoc
fi
jazzy_dir="$root_path/.build/jazzy"
rm -rf "$jazzy_dir"
mkdir -p "$jazzy_dir"
module_switcher="$jazzy_dir/README.md"
jazzy_args=(--clean
--author 'SwiftMetrics team'
--readme "$module_switcher"
--author_url https://github.com/apple/swift-metrics
--github_url https://github.com/apple/swift-metrics
--github-file-prefix https://github.com/apple/swift-metrics/tree/$version
--theme fullwidth
--xcodebuild-arguments -scheme,swift-metrics-Package)
cat > "$module_switcher" <<"EOF"
# SwiftMetrics Docs
SwiftMetrics is a Swift metrics API package.
To get started with SwiftMetrics, [`import Metrics`](../CoreMetrics/index.html). The most important types are:
* [`Counter`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Counter.html)
* [`Timer`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Timer.html)
* [`Recorder`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Recorder.html)
* [`Gauge`](https://apple.github.io/swift-metrics/docs/current/CoreMetrics/Classes/Gauge.html)
SwiftMetrics contains multiple modules:
EOF
for module in "${modules[@]}"; do
echo " - [$module](../$module/index.html)" >> "$module_switcher"
done
for module in "${modules[@]}"; do
echo "processing $module"
args=("${jazzy_args[@]}" --output "$jazzy_dir/docs/$version/$module" --docset-path "$jazzy_dir/docset/$version/$module"
--module "$module" --module-version $version
--root-url "https://apple.github.io/swift-metrics/docs/$version/$module/")
if [[ -f "$root_path/.build/sourcekitten/$module.json" ]]; then
args+=(--sourcekitten-sourcefile "$root_path/.build/sourcekitten/$module.json")
fi
jazzy "${args[@]}"
done
# push to github pages
if [[ $PUSH == true ]]; then
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
GIT_AUTHOR=$(git --no-pager show -s --format='%an <%ae>' HEAD)
git fetch origin +gh-pages:gh-pages
git checkout gh-pages
rm -rf "docs/$version"
rm -rf "docs/current"
cp -r "$jazzy_dir/docs/$version" docs/
cp -r "docs/$version" docs/current
git add --all docs
echo '<html><head><meta http-equiv="refresh" content="0; url=docs/current/CoreMetrics/index.html" /></head></html>' > index.html
git add index.html
touch .nojekyll
git add .nojekyll
changes=$(git diff-index --name-only HEAD)
if [[ -n "$changes" ]]; then
echo -e "changes detected\n$changes"
git commit --author="$GIT_AUTHOR" -m "publish $version docs"
git push origin gh-pages
else
echo "no changes detected"
fi
git checkout -f $BRANCH_NAME
fi

231
scripts/generate_linux_tests.rb Executable file
View File

@ -0,0 +1,231 @@
#!/usr/bin/env ruby
#
# process_test_files.rb
#
# Copyright 2016 Tony Stone
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Created by Tony Stone on 5/4/16.
#
require 'getoptlong'
require 'fileutils'
require 'pathname'
include FileUtils
#
# This ruby script will auto generate LinuxMain.swift and the +XCTest.swift extension files for Swift Package Manager on Linux platforms.
#
# See https://github.com/apple/swift-corelibs-xctest/blob/master/Documentation/Linux.md
#
def header(fileName)
string = <<-eos
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// <FileName>
//
import XCTest
///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
eos
string
.sub('<FileName>', File.basename(fileName))
.sub('<Date>', Time.now.to_s)
end
def createExtensionFile(fileName, classes)
extensionFile = fileName.sub! '.swift', '+XCTest.swift'
print 'Creating file: ' + extensionFile + "\n"
File.open(extensionFile, 'w') do |file|
file.write header(extensionFile)
file.write "\n"
for classArray in classes
file.write 'extension ' + classArray[0] + " {\n"
file.write ' static var allTests: [(String, (' + classArray[0] + ") -> () throws -> Void)] {\n"
file.write " return [\n"
for funcName in classArray[1]
file.write ' ("' + funcName + '", ' + funcName + "),\n"
end
file.write " ]\n"
file.write " }\n"
file.write "}\n"
end
end
end
def createLinuxMain(testsDirectory, allTestSubDirectories, files)
fileName = testsDirectory + '/LinuxMain.swift'
print 'Creating file: ' + fileName + "\n"
File.open(fileName, 'w') do |file|
file.write header(fileName)
file.write "\n"
file.write "#if os(Linux) || os(FreeBSD)\n"
for testSubDirectory in allTestSubDirectories.sort { |x, y| x <=> y }
file.write '@testable import ' + testSubDirectory + "\n"
end
file.write "\n"
file.write "XCTMain([\n"
testCases = []
for classes in files
for classArray in classes
testCases << classArray[0]
end
end
for testCase in testCases.sort { |x, y| x <=> y }
file.write ' testCase(' + testCase + ".allTests),\n"
end
file.write "])\n"
file.write "#endif\n"
end
end
def parseSourceFile(fileName)
puts 'Parsing file: ' + fileName + "\n"
classes = []
currentClass = nil
inIfLinux = false
inElse = false
ignore = false
#
# Read the file line by line
# and parse to find the class
# names and func names
#
File.readlines(fileName).each do |line|
if inIfLinux
if /\#else/.match(line)
inElse = true
ignore = true
else
if /\#end/.match(line)
inElse = false
inIfLinux = false
ignore = false
end
end
else
if /\#if[ \t]+os\(Linux\)/.match(line)
inIfLinux = true
ignore = false
end
end
next if ignore
# Match class or func
match = line[/class[ \t]+[a-zA-Z0-9_]*(?=[ \t]*:[ \t]*XCTestCase)|func[ \t]+test[a-zA-Z0-9_]*(?=[ \t]*\(\))/, 0]
if match
if match[/class/, 0] == 'class'
className = match.sub(/^class[ \t]+/, '')
#
# Create a new class / func structure
# and add it to the classes array.
#
currentClass = [className, []]
classes << currentClass
else # Must be a func
funcName = match.sub(/^func[ \t]+/, '')
#
# Add each func name the the class / func
# structure created above.
#
currentClass[1] << funcName
end
end
end
classes
end
#
# Main routine
#
#
testsDirectory = 'Tests'
options = GetoptLong.new(['--tests-dir', GetoptLong::OPTIONAL_ARGUMENT])
options.quiet = true
begin
options.each do |option, value|
case option
when '--tests-dir'
testsDirectory = value
end
end
rescue GetoptLong::InvalidOption
end
allTestSubDirectories = []
allFiles = []
Dir[testsDirectory + '/*'].each do |subDirectory|
next unless File.directory?(subDirectory)
directoryHasClasses = false
Dir[subDirectory + '/*Test{s,}.swift'].each do |fileName|
next unless File.file? fileName
fileClasses = parseSourceFile(fileName)
#
# If there are classes in the
# test source file, create an extension
# file for it.
#
next unless fileClasses.count > 0
createExtensionFile(fileName, fileClasses)
directoryHasClasses = true
allFiles << fileClasses
end
if directoryHasClasses
allTestSubDirectories << Pathname.new(subDirectory).split.last.to_s
end
end
#
# Last step is the create a LinuxMain.swift file that
# references all the classes and funcs in the source files.
#
if allFiles.count > 0
createLinuxMain(testsDirectory, allTestSubDirectories, allFiles)
end
# eof

153
scripts/sanity.sh Executable file
View File

@ -0,0 +1,153 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) 2018-2019 Apple Inc. and the Swift Metrics API project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2019 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
function replace_acceptable_years() {
# this needs to replace all acceptable forms with 'YEARS'
sed -e 's/2018-2019/YEARS/' -e 's/2019/YEARS/'
}
printf "=> Checking linux tests... "
FIRST_OUT="$(git status --porcelain)"
ruby "$here/../scripts/generate_linux_tests.rb" > /dev/null
SECOND_OUT="$(git status --porcelain)"
if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then
printf "\033[0;31mmissing changes!\033[0m\n"
git --no-pager diff
exit 1
else
printf "\033[0;32mokay.\033[0m\n"
fi
printf "=> Checking format... "
FIRST_OUT="$(git status --porcelain)"
swiftformat . > /dev/null 2>&1
SECOND_OUT="$(git status --porcelain)"
if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then
printf "\033[0;31mformatting issues!\033[0m\n"
git --no-pager diff
exit 1
else
printf "\033[0;32mokay.\033[0m\n"
fi
printf "=> Checking license headers\n"
tmp=$(mktemp /tmp/.swift-metrics-sanity_XXXXXX)
for language in swift-or-c bash dtrace; do
printf " * $language... "
declare -a matching_files
declare -a exceptions
expections=( )
matching_files=( -name '*' )
case "$language" in
swift-or-c)
exceptions=( -name c_nio_http_parser.c -o -name c_nio_http_parser.h -o -name cpp_magic.h -o -name Package.swift -o -name CNIOSHA1.h -o -name c_nio_sha1.c -o -name ifaddrs-android.c -o -name ifaddrs-android.h)
matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' )
cat > "$tmp" <<"EOF"
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) YEARS Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
EOF
;;
bash)
matching_files=( -name '*.sh' )
cat > "$tmp" <<"EOF"
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift Metrics API open source project
##
## Copyright (c) YEARS Apple Inc. and the Swift Metrics API project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
EOF
;;
dtrace)
matching_files=( -name '*.d' )
cat > "$tmp" <<"EOF"
#!/usr/sbin/dtrace -q -s
/*===----------------------------------------------------------------------===*
*
* This source file is part of the Swift Metrics API open source project
*
* Copyright (c) YEARS Apple Inc. and the Swift Metrics API project authors
* Licensed under Apache License v2.0
*
* See LICENSE.txt for license information
* See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
*
* SPDX-License-Identifier: Apache-2.0
*
*===----------------------------------------------------------------------===*/
EOF
;;
*)
echo >&2 "ERROR: unknown language '$language'"
;;
esac
expected_lines=$(cat "$tmp" | wc -l)
expected_sha=$(cat "$tmp" | shasum)
(
cd "$here/.."
find . \
\( \! -path './.build/*' -a \
\( "${matching_files[@]}" \) -a \
\( \! \( "${exceptions[@]}" \) \) \) | while read line; do
if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then
printf "\033[0;31mmissing headers in file '$line'!\033[0m\n"
diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp"
exit 1
fi
done
printf "\033[0;32mokay.\033[0m\n"
)
done
rm "$tmp"