Compare commits
82 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
4c83e1cdf4 | |
|
|
98d36172c1 | |
|
|
27ecca7ac1 | |
|
|
071d1cac3c | |
|
|
0556b16079 | |
|
|
44491db7cc | |
|
|
3c0f419970 | |
|
|
cbd39ceaca | |
|
|
029e902273 | |
|
|
58f390a873 | |
|
|
53de3bfa9a | |
|
|
5e63558d12 | |
|
|
726392cf4e | |
|
|
4a491b5ad5 | |
|
|
58e2968a7f | |
|
|
4ec5a219dc | |
|
|
c1976209b5 | |
|
|
cffefdc627 | |
|
|
d720898dbf | |
|
|
569db3a632 | |
|
|
e0165b53d4 | |
|
|
d067b0e0f7 | |
|
|
79e5fb4fe4 | |
|
|
ce594e71e9 | |
|
|
0124d9ad5c | |
|
|
eb18581491 | |
|
|
33d3f71e00 | |
|
|
7e53749103 | |
|
|
9c0646af4d | |
|
|
13ea1fe7fc | |
|
|
971ba26378 | |
|
|
bf7ea93e17 | |
|
|
3402510406 | |
|
|
8bcdb6e82b | |
|
|
9d5ff3d48f | |
|
|
32eef8ae84 | |
|
|
862b99bc11 | |
|
|
78b6238009 | |
|
|
a79936ffc1 | |
|
|
e8bced74bc | |
|
|
cbfde655cf | |
|
|
9b39d811a8 | |
|
|
bcea8c19fe | |
|
|
bd1b935c8e | |
|
|
53be78637e | |
|
|
d885a4f5e9 | |
|
|
fd0ee6956b | |
|
|
1c1408bf8f | |
|
|
eadb828f87 | |
|
|
e00284be24 | |
|
|
ea66bbc2b5 | |
|
|
d09e751437 | |
|
|
6bc8aa8a06 | |
|
|
495aca6d51 | |
|
|
992b87907d | |
|
|
3edd2f57af | |
|
|
2c58b010a2 | |
|
|
42372a8598 | |
|
|
99a068b962 | |
|
|
8ea359b532 | |
|
|
2a7fd99ea8 | |
|
|
1bb953eb76 | |
|
|
e2c4a510b6 | |
|
|
c6cdc69235 | |
|
|
de1a7d570b | |
|
|
923775a5f6 | |
|
|
68e6cb2938 | |
|
|
c793c35d07 | |
|
|
7274cf41ef | |
|
|
e382458581 | |
|
|
2b6e31e1b3 | |
|
|
f5ed78cb26 | |
|
|
44e8bfc7f5 | |
|
|
5702ee1174 | |
|
|
cf757fe4eb | |
|
|
69cc955761 | |
|
|
b6c9d615de | |
|
|
f46b1894aa | |
|
|
313c84fdec | |
|
|
56ae451f00 | |
|
|
41d2db7675 | |
|
|
a8db098592 |
|
|
@ -0,0 +1,8 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
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
|
||||
|
|
@ -6,3 +6,4 @@
|
|||
.SourceKitten
|
||||
*.orig
|
||||
.swiftpm
|
||||
Package.resolved
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
.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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
version: 1
|
||||
builder:
|
||||
configs:
|
||||
- documentation_targets: [CoreMetrics, Metrics, MetricsTestKit]
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"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
13
.swiftformat
|
|
@ -1,13 +0,0 @@
|
|||
# file options
|
||||
|
||||
--exclude .build
|
||||
|
||||
# format options
|
||||
|
||||
--self insert
|
||||
--patternlet inline
|
||||
--stripunusedargs unnamed-only
|
||||
--comments ignore
|
||||
--ifdef no-indent
|
||||
|
||||
# rules
|
||||
|
|
@ -1,55 +1,3 @@
|
|||
# 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.
|
||||
|
||||
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 other’s 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.
|
||||
The code of conduct for this project can be found at https://swift.org/code-of-conduct.
|
||||
|
|
|
|||
|
|
@ -60,10 +60,9 @@ We require that your commit messages match our template. The easiest way to do t
|
|||
|
||||
git config commit.template dev/git.commit.template
|
||||
|
||||
### Make sure Tests work on Linux
|
||||
### Run CI checks 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.
|
||||
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).
|
||||
|
||||
## How to contribute your work
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// swift-tools-version:4.2
|
||||
// swift-tools-version:5.9
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Swift Metrics API open source project
|
||||
|
|
@ -20,19 +20,45 @@ let package = Package(
|
|||
products: [
|
||||
.library(name: "CoreMetrics", targets: ["CoreMetrics"]),
|
||||
.library(name: "Metrics", targets: ["Metrics"]),
|
||||
.library(name: "MetricsTestKit", targets: ["MetricsTestKit"]),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CoreMetrics",
|
||||
dependencies: []
|
||||
name: "CoreMetrics"
|
||||
),
|
||||
.target(
|
||||
name: "Metrics",
|
||||
dependencies: ["CoreMetrics"]
|
||||
),
|
||||
.target(
|
||||
name: "MetricsTestKit",
|
||||
dependencies: ["Metrics"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "MetricsTests",
|
||||
dependencies: ["Metrics"]
|
||||
dependencies: ["Metrics", "MetricsTestKit"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
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
125
README.md
|
|
@ -15,15 +15,20 @@ 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
|
||||
// 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"),
|
||||
// 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: ["Metrics"]),
|
||||
.target(
|
||||
name: "BestExampleApp",
|
||||
dependencies: [
|
||||
// ...
|
||||
.product(name: "Metrics", package: "swift-metrics"),
|
||||
]
|
||||
),
|
||||
```
|
||||
|
||||
### Emitting metrics information
|
||||
|
|
@ -57,8 +62,13 @@ 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
|
||||
|
|
@ -80,7 +90,7 @@ This API was designed with the contributors to the Swift on Server community and
|
|||
|
||||
### Metric types
|
||||
|
||||
The API supports four metric types:
|
||||
The API supports six 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.
|
||||
|
||||
|
|
@ -88,18 +98,30 @@ The API supports four 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
|
||||
|
|
@ -110,7 +132,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`, `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:
|
||||
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:
|
||||
|
||||
```swift
|
||||
let metricsImplementation = MyFavoriteMetricsImplementation()
|
||||
|
|
@ -124,10 +146,12 @@ 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 makeRecorder(label: String, dimensions: [(String, String)], aggregate: Bool) -> RecorderHandler
|
||||
func makeMeter(label: String, dimensions: [(String, String)]) -> MeterHandler
|
||||
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)
|
||||
}
|
||||
|
|
@ -144,11 +168,14 @@ public protocol CounterHandler: AnyObject {
|
|||
}
|
||||
```
|
||||
|
||||
**Timer**
|
||||
**Meter**
|
||||
|
||||
```swift
|
||||
public protocol TimerHandler: AnyObject {
|
||||
func recordNanoseconds(_ duration: Int64)
|
||||
public protocol MeterHandler: AnyObject {
|
||||
func set(_ value: Int64)
|
||||
func set(_ value: Double)
|
||||
func increment(by: Double)
|
||||
func decrement(by: Double)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -161,9 +188,17 @@ public protocol RecorderHandler: AnyObject {
|
|||
}
|
||||
```
|
||||
|
||||
**Timer**
|
||||
|
||||
```swift
|
||||
public protocol TimerHandler: AnyObject {
|
||||
func recordNanoseconds(_ duration: Int64)
|
||||
}
|
||||
```
|
||||
|
||||
#### Dealing with Overflows
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```swift
|
||||
class ExampleCounter: CounterHandler {
|
||||
|
|
@ -191,9 +226,12 @@ 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 {
|
||||
let maker: (String, [(String, String)]) -> RecorderHandler = aggregate ? ExampleRecorder.init : ExampleGauge.init
|
||||
return maker(label, dimensions)
|
||||
return ExampleRecorder(label, dimensions, aggregate)
|
||||
}
|
||||
|
||||
func makeTimer(label: String, dimensions: [(String, String)]) -> TimerHandler {
|
||||
|
|
@ -202,7 +240,8 @@ class SimpleMetricsLibrary: MetricsFactory {
|
|||
|
||||
// implementation is stateless, so nothing to do on destroy calls
|
||||
func destroyCounter(_ handler: CounterHandler) {}
|
||||
func destroyRecorder(_ handler: RecorderHandler) {}
|
||||
func destroyMeter(_ handler: TimerHandler) {}
|
||||
func destroyRecorder(_ handler: RecorderHandler) {}
|
||||
func destroyTimer(_ handler: TimerHandler) {}
|
||||
|
||||
private class ExampleCounter: CounterHandler {
|
||||
|
|
@ -223,9 +262,32 @@ class SimpleMetricsLibrary: MetricsFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private class ExampleRecorder: RecorderHandler {
|
||||
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) {}
|
||||
|
||||
private let lock = NSLock()
|
||||
var values = [(Int64, Double)]()
|
||||
func record(_ value: Int64) {
|
||||
|
|
@ -264,26 +326,23 @@ class SimpleMetricsLibrary: MetricsFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private class ExampleGauge: RecorderHandler {
|
||||
private class ExampleTimer: TimerHandler {
|
||||
init(_: String, _: [(String, String)]) {}
|
||||
|
||||
let lock = NSLock()
|
||||
var _value: Double = 0
|
||||
func record(_ value: Int64) {
|
||||
self.record(Double(value))
|
||||
}
|
||||
var _value: Int64 = 0
|
||||
|
||||
func record(_ value: Double) {
|
||||
self.lock.withLock { _value = value }
|
||||
}
|
||||
}
|
||||
|
||||
private class ExampleTimer: ExampleRecorder, TimerHandler {
|
||||
func recordNanoseconds(_ duration: Int64) {
|
||||
super.record(duration)
|
||||
self.lock.withLock { _value = 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# 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).
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# 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/
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# ``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``
|
||||
|
||||
|
|
@ -26,29 +26,58 @@
|
|||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
|
||||
#if canImport(WASILibc)
|
||||
// No locking on WASILibc
|
||||
#elseif canImport(Darwin)
|
||||
import Darwin
|
||||
#else
|
||||
#elseif os(Windows)
|
||||
import WinSDK
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Android)
|
||||
import Android
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#else
|
||||
#error("Unsupported runtime")
|
||||
#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.
|
||||
/// one used by NIO. On Windows, the lock is based on the substantially similar
|
||||
/// `SRWLOCK` type.
|
||||
internal final class Lock {
|
||||
fileprivate let mutex: UnsafeMutablePointer<pthread_mutex_t> = UnsafeMutablePointer.allocate(capacity: 1)
|
||||
#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
|
||||
|
||||
/// Create a new lock.
|
||||
public init() {
|
||||
let err = pthread_mutex_init(self.mutex, nil)
|
||||
precondition(err == 0, "pthread_mutex_init failed with error \(err)")
|
||||
#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
|
||||
}
|
||||
|
||||
deinit {
|
||||
#if os(Windows)
|
||||
// SRWLOCK does not need to be free'd
|
||||
#else
|
||||
let err = pthread_mutex_destroy(self.mutex)
|
||||
precondition(err == 0, "pthread_mutex_destroy failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
|
||||
#endif
|
||||
self.mutex.deallocate()
|
||||
}
|
||||
|
||||
|
|
@ -57,8 +86,12 @@ 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, "pthread_mutex_lock failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Release the lock.
|
||||
|
|
@ -66,8 +99,12 @@ 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, "pthread_mutex_unlock failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +118,7 @@ extension Lock {
|
|||
/// - Parameter body: The block to execute while holding the lock.
|
||||
/// - Returns: The value returned by the block.
|
||||
@inlinable
|
||||
internal func withLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
func withLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
self.lock()
|
||||
defer {
|
||||
self.unlock()
|
||||
|
|
@ -91,70 +128,119 @@ extension Lock {
|
|||
|
||||
// specialise Void return (for performance)
|
||||
@inlinable
|
||||
internal func withLockVoid(_ body: () throws -> Void) rethrows {
|
||||
func withLockVoid(_ body: () throws -> Void) rethrows {
|
||||
try self.withLock(body)
|
||||
}
|
||||
}
|
||||
|
||||
/// A threading lock based on `libpthread` instead of `libdispatch`.
|
||||
extension Lock: @unchecked Sendable {}
|
||||
|
||||
/// A reader/writer threading lock based on `libpthread` instead of `libdispatch`.
|
||||
///
|
||||
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
|
||||
/// This object provides a lock on top of a single `pthread_rwlock_t`. This kind
|
||||
/// of lock is safe to use with `libpthread`-based threading models, such as the
|
||||
/// one used by NIO.
|
||||
/// one used by NIO. On Windows, the lock is based on the substantially similar
|
||||
/// `SRWLOCK` type.
|
||||
internal final class ReadWriteLock {
|
||||
fileprivate let rwlock: UnsafeMutablePointer<pthread_rwlock_t> = UnsafeMutablePointer.allocate(capacity: 1)
|
||||
#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
|
||||
|
||||
/// 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, "pthread_rwlock_init failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
|
||||
#endif
|
||||
}
|
||||
|
||||
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, "pthread_rwlock_destroy failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
|
||||
#endif
|
||||
self.rwlock.deallocate()
|
||||
}
|
||||
|
||||
/// Acquire a reader lock.
|
||||
///
|
||||
/// Whenever possible, consider using `withLock` instead of this method and
|
||||
/// `unlock`, to simplify lock handling.
|
||||
/// Whenever possible, consider using `withReaderLock` 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, "pthread_rwlock_rdlock failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Acquire a writer lock.
|
||||
///
|
||||
/// Whenever possible, consider using `withLock` instead of this method and
|
||||
/// `unlock`, to simplify lock handling.
|
||||
/// Whenever possible, consider using `withWriterLock` 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, "pthread_rwlock_wrlock failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Release the lock.
|
||||
///
|
||||
/// Whenever possible, consider using `withLock` instead of this method and
|
||||
/// `lock`, to simplify lock handling.
|
||||
/// Whenever possible, consider using `withReaderLock` and `withWriterLock`
|
||||
/// instead of this method and `lockRead` and `lockWrite`, 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, "pthread_rwlock_unlock failed with error \(err)")
|
||||
precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension ReadWriteLock {
|
||||
/// Acquire the reader lock for the duration of the given block.
|
||||
///
|
||||
/// 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.
|
||||
/// 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.
|
||||
///
|
||||
/// - Parameter body: The block to execute while holding the lock.
|
||||
/// - Parameter body: The block to execute while holding the reader lock.
|
||||
/// - Returns: The value returned by the block.
|
||||
@inlinable
|
||||
internal func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
func withReaderLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
self.lockRead()
|
||||
defer {
|
||||
self.unlock()
|
||||
|
|
@ -164,14 +250,14 @@ extension ReadWriteLock {
|
|||
|
||||
/// Acquire the writer lock for the duration of the given block.
|
||||
///
|
||||
/// 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.
|
||||
/// 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.
|
||||
///
|
||||
/// - Parameter body: The block to execute while holding the lock.
|
||||
/// - Parameter body: The block to execute while holding the writer lock.
|
||||
/// - Returns: The value returned by the block.
|
||||
@inlinable
|
||||
internal func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
func withWriterLock<T>(_ body: () throws -> T) rethrows -> T {
|
||||
self.lockWrite()
|
||||
defer {
|
||||
self.unlock()
|
||||
|
|
@ -181,13 +267,15 @@ extension ReadWriteLock {
|
|||
|
||||
// specialise Void return (for performance)
|
||||
@inlinable
|
||||
internal func withReaderLockVoid(_ body: () throws -> Void) rethrows {
|
||||
func withReaderLockVoid(_ body: () throws -> Void) rethrows {
|
||||
try self.withReaderLock(body)
|
||||
}
|
||||
|
||||
// specialise Void return (for performance)
|
||||
@inlinable
|
||||
internal func withWriterLockVoid(_ body: () throws -> Void) rethrows {
|
||||
func withWriterLockVoid(_ body: () throws -> Void) rethrows {
|
||||
try self.withWriterLock(body)
|
||||
}
|
||||
}
|
||||
|
||||
extension ReadWriteLock: @unchecked Sendable {}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,5 @@
|
|||
# ``Metrics``
|
||||
|
||||
A Metrics API package for Swift.
|
||||
|
||||
Refer to `CoreMetrics` module documentation for the majority of types.
|
||||
|
|
@ -11,12 +11,16 @@
|
|||
// 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
|
||||
|
||||
public extension Timer {
|
||||
@_exported import class CoreMetrics.Timer
|
||||
|
||||
extension Timer {
|
||||
/// Convenience for measuring duration of a closure.
|
||||
///
|
||||
/// - parameters:
|
||||
|
|
@ -24,7 +28,11 @@ public extension Timer {
|
|||
/// - dimensions: The dimensions for the Timer.
|
||||
/// - body: Closure to run & record.
|
||||
@inlinable
|
||||
static func measure<T>(label: String, dimensions: [(String, String)] = [], body: @escaping () throws -> T) rethrows -> T {
|
||||
public 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 {
|
||||
|
|
@ -33,15 +41,24 @@ public 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)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Timer {
|
||||
extension Timer {
|
||||
/// Convenience for recording a duration based on TimeInterval.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - duration: The duration to record.
|
||||
@inlinable
|
||||
func record(_ duration: TimeInterval) {
|
||||
public func record(_ duration: TimeInterval) {
|
||||
self.recordSeconds(duration)
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +67,17 @@ public extension Timer {
|
|||
/// - parameters:
|
||||
/// - duration: The duration to record.
|
||||
@inlinable
|
||||
func record(_ duration: DispatchTimeInterval) {
|
||||
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)
|
||||
switch duration {
|
||||
case .nanoseconds(let value):
|
||||
self.recordNanoseconds(value)
|
||||
|
|
@ -62,6 +89,72 @@ public 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# ``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``
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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 {}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -12,28 +12,30 @@
|
|||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
@testable import CoreMetrics
|
||||
import MetricsTestKit
|
||||
import XCTest
|
||||
|
||||
@testable import CoreMetrics
|
||||
|
||||
class MetricsTests: XCTestCase {
|
||||
func testCounters() throws {
|
||||
// bootstrap with our test metrics
|
||||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
let group = DispatchGroup()
|
||||
let name = "counter-\(NSUUID().uuidString)"
|
||||
let name = "counter-\(UUID().uuidString)"
|
||||
let counter = Counter(label: name)
|
||||
let testCounter = counter.handler as! TestCounter
|
||||
let total = Int.random(in: 500 ... 1000)
|
||||
for _ in 0 ... total {
|
||||
let testCounter = try metrics.expectCounter(counter)
|
||||
let total = Int.random(in: 500...1000)
|
||||
for _ in 0..<total {
|
||||
group.enter()
|
||||
DispatchQueue(label: "\(name)-queue").async {
|
||||
counter.increment(by: Int.random(in: 0 ... 1000))
|
||||
group.leave()
|
||||
defer { group.leave() }
|
||||
counter.increment(by: Int.random(in: 0...1000))
|
||||
}
|
||||
}
|
||||
group.wait()
|
||||
XCTAssertEqual(testCounter.values.count - 1, total, "expected number of entries to match")
|
||||
XCTAssertEqual(testCounter.values.count, total, "expected number of entries to match")
|
||||
testCounter.reset()
|
||||
XCTAssertEqual(testCounter.values.count, 0, "expected number of entries to match")
|
||||
}
|
||||
|
|
@ -43,34 +45,126 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let name = "counter-\(NSUUID().uuidString)"
|
||||
let value = Int.random(in: Int.min ... Int.max)
|
||||
let name = "counter-\(UUID().uuidString)"
|
||||
let value = Int.random(in: Int.min...Int.max)
|
||||
Counter(label: name).increment(by: value)
|
||||
let counter = metrics.counters[name] as! TestCounter
|
||||
let counter = try metrics.expectCounter(name)
|
||||
XCTAssertEqual(counter.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(counter.values[0].1, Int64(value), "expected value to match")
|
||||
XCTAssertEqual(counter.values[0], 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-\(NSUUID().uuidString)"
|
||||
let name = "recorder-\(UUID().uuidString)"
|
||||
let recorder = Recorder(label: name)
|
||||
let testRecorder = recorder.handler as! TestRecorder
|
||||
let total = Int.random(in: 500 ... 1000)
|
||||
for _ in 0 ... total {
|
||||
let testRecorder = try metrics.expectRecorder(recorder)
|
||||
let total = Int.random(in: 500...1000)
|
||||
for _ in 0..<total {
|
||||
group.enter()
|
||||
DispatchQueue(label: "\(name)-queue").async {
|
||||
recorder.record(Int.random(in: Int.min ... Int.max))
|
||||
group.leave()
|
||||
defer { group.leave() }
|
||||
recorder.record(Int.random(in: Int.min...Int.max))
|
||||
}
|
||||
}
|
||||
group.wait()
|
||||
XCTAssertEqual(testRecorder.values.count - 1, total, "expected number of entries to match")
|
||||
XCTAssertEqual(testRecorder.values.count, total, "expected number of entries to match")
|
||||
}
|
||||
|
||||
func testRecordersInt() throws {
|
||||
|
|
@ -78,14 +172,14 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
let recorder = Recorder(label: "test-recorder")
|
||||
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 {
|
||||
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 {
|
||||
recorder.record(values[i])
|
||||
}
|
||||
XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
|
||||
for i in 0 ... values.count - 1 {
|
||||
XCTAssertEqual(Int32(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
|
||||
for i in 0..<values.count {
|
||||
XCTAssertEqual(Int32(testRecorder.values[i]), values[i], "expected value #\(i) to match.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,14 +188,14 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
let recorder = Recorder(label: "test-recorder")
|
||||
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 {
|
||||
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 {
|
||||
recorder.record(values[i])
|
||||
}
|
||||
XCTAssertEqual(values.count, testRecorder.values.count, "expected number of entries to match")
|
||||
for i in 0 ... values.count - 1 {
|
||||
XCTAssertEqual(Float(testRecorder.values[i].1), values[i], "expected value #\(i) to match.")
|
||||
for i in 0..<values.count {
|
||||
XCTAssertEqual(Float(testRecorder.values[i]), values[i], "expected value #\(i) to match.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,12 +204,12 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let name = "recorder-\(NSUUID().uuidString)"
|
||||
let value = Double.random(in: Double(Int.min) ... Double(Int.max))
|
||||
let name = "recorder-\(UUID().uuidString)"
|
||||
let value = Double.random(in: Double(Int.min)...Double(Int.max))
|
||||
Recorder(label: name).record(value)
|
||||
let recorder = metrics.recorders[name] as! TestRecorder
|
||||
let recorder = try metrics.expectRecorder(name)
|
||||
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
|
||||
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
|
||||
}
|
||||
|
||||
func testTimers() throws {
|
||||
|
|
@ -123,19 +217,19 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
let group = DispatchGroup()
|
||||
let name = "timer-\(NSUUID().uuidString)"
|
||||
let name = "timer-\(UUID().uuidString)"
|
||||
let timer = Timer(label: name)
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let total = Int.random(in: 500 ... 1000)
|
||||
for _ in 0 ... total {
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
let total = Int.random(in: 500...1000)
|
||||
for _ in 0..<total {
|
||||
group.enter()
|
||||
DispatchQueue(label: "\(name)-queue").async {
|
||||
timer.recordNanoseconds(Int64.random(in: Int64.min ... Int64.max))
|
||||
group.leave()
|
||||
defer { group.leave() }
|
||||
timer.recordNanoseconds(Int64.random(in: Int64.min...Int64.max))
|
||||
}
|
||||
}
|
||||
group.wait()
|
||||
XCTAssertEqual(testTimer.values.count - 1, total, "expected number of entries to match")
|
||||
XCTAssertEqual(testTimer.values.count, total, "expected number of entries to match")
|
||||
}
|
||||
|
||||
func testTimerBlock() throws {
|
||||
|
|
@ -143,12 +237,12 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let name = "timer-\(NSUUID().uuidString)"
|
||||
let value = Int64.random(in: Int64.min ... Int64.max)
|
||||
let name = "timer-\(UUID().uuidString)"
|
||||
let value = Int64.random(in: Int64.min...Int64.max)
|
||||
Timer(label: name).recordNanoseconds(value)
|
||||
let timer = metrics.timers[name] as! TestTimer
|
||||
let timer = try metrics.expectTimer(name)
|
||||
XCTAssertEqual(timer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(timer.values[0].1, value, "expected value to match")
|
||||
XCTAssertEqual(timer.values[0], value, "expected value to match")
|
||||
}
|
||||
|
||||
func testTimerVariants() throws {
|
||||
|
|
@ -157,27 +251,27 @@ class MetricsTests: XCTestCase {
|
|||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let timer = Timer(label: "test-timer")
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
// 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].1, nano, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[0], 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].1, micro * 1000, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[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].1, milli * 1_000_000, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[2], 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].1, sec * 1_000_000_000, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[3], sec * 1_000_000_000, "expected value to match")
|
||||
}
|
||||
|
||||
func testTimerOverflow() throws {
|
||||
|
|
@ -186,35 +280,35 @@ class MetricsTests: XCTestCase {
|
|||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let timer = Timer(label: "test-timer")
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
// nano (integer)
|
||||
timer.recordNanoseconds(Int64.max)
|
||||
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(testTimer.values[0].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[0], 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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[2], 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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[2], 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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[3], 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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
|
||||
}
|
||||
|
||||
func testTimerHandlesUnsignedOverflow() throws {
|
||||
|
|
@ -223,23 +317,23 @@ class MetricsTests: XCTestCase {
|
|||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let timer = Timer(label: "test-timer")
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
// nano
|
||||
timer.recordNanoseconds(UInt64.max)
|
||||
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(testTimer.values[0].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[0], 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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[2], 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].1, Int64.max, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[3], Int64.max, "expected value to match")
|
||||
}
|
||||
|
||||
func testGauge() throws {
|
||||
|
|
@ -247,13 +341,13 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let name = "gauge-\(NSUUID().uuidString)"
|
||||
let value = Double.random(in: -1000 ... 1000)
|
||||
let name = "gauge-\(UUID().uuidString)"
|
||||
let value = Double.random(in: -1000...1000)
|
||||
let gauge = Gauge(label: name)
|
||||
gauge.record(value)
|
||||
let recorder = gauge.handler as! TestRecorder
|
||||
let recorder = try metrics.expectRecorder(gauge)
|
||||
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
|
||||
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
|
||||
}
|
||||
|
||||
func testGaugeBlock() throws {
|
||||
|
|
@ -261,61 +355,346 @@ class MetricsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let name = "gauge-\(NSUUID().uuidString)"
|
||||
let value = Double.random(in: -1000 ... 1000)
|
||||
let name = "gauge-\(UUID().uuidString)"
|
||||
let value = Double.random(in: -1000...1000)
|
||||
Gauge(label: name).record(value)
|
||||
let recorder = metrics.recorders[name] as! TestRecorder
|
||||
let recorder = try metrics.expectRecorder(name)
|
||||
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorder.values[0].1, value, "expected value to match")
|
||||
XCTAssertEqual(recorder.lastValue, value, "expected value to match")
|
||||
}
|
||||
|
||||
func testMUX() throws {
|
||||
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 {
|
||||
// bootstrap with our test metrics
|
||||
let factories = [TestMetrics(), TestMetrics(), TestMetrics()]
|
||||
MetricsSystem.bootstrapInternal(MultiplexMetricsHandler(factories: factories))
|
||||
// run the test
|
||||
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")
|
||||
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")
|
||||
}
|
||||
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")
|
||||
muxCounter.reset()
|
||||
for factory in factories {
|
||||
let counter = factory.counters.first
|
||||
XCTAssertEqual(counter?.values.count, 0, "expected number of entries to match")
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomFactory() {
|
||||
class CustomHandler: CounterHandler {
|
||||
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 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")
|
||||
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)
|
||||
}
|
||||
|
||||
func testDestroyingGauge() throws {
|
||||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
|
||||
let name = "gauge-\(NSUUID().uuidString)"
|
||||
let value = Double.random(in: -1000 ... 1000)
|
||||
let name = "gauge-\(UUID().uuidString)"
|
||||
let value = Double.random(in: -1000...1000)
|
||||
|
||||
let gauge = Gauge(label: name)
|
||||
gauge.record(value)
|
||||
|
||||
let recorder = gauge.handler as! TestRecorder
|
||||
let recorder = try metrics.expectRecorder(gauge)
|
||||
XCTAssertEqual(recorder.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorder.values.first!.1, value, "expected value to match")
|
||||
XCTAssertEqual(recorder.values.first, value, "expected value to match")
|
||||
XCTAssertEqual(metrics.recorders.count, 1, "recorder should have been stored")
|
||||
|
||||
let identity = ObjectIdentifier(recorder)
|
||||
|
|
@ -325,27 +704,65 @@ class MetricsTests: XCTestCase {
|
|||
let gaugeAgain = Gauge(label: name)
|
||||
gaugeAgain.record(-value)
|
||||
|
||||
let recorderAgain = gaugeAgain.handler as! TestRecorder
|
||||
let recorderAgain = try metrics.expectRecorder(gaugeAgain)
|
||||
XCTAssertEqual(recorderAgain.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(recorderAgain.values.first!.1, -value, "expected value to match")
|
||||
XCTAssertEqual(recorderAgain.values.first, -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")
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
func testDestroyingCounter() throws {
|
||||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
|
||||
let name = "counter-\(NSUUID().uuidString)"
|
||||
let value = Int.random(in: 0 ... 1000)
|
||||
let name = "counter-\(UUID().uuidString)"
|
||||
let value = Int.random(in: 0...1000)
|
||||
|
||||
let counter = Counter(label: name)
|
||||
counter.increment(by: value)
|
||||
|
||||
let testCounter = counter.handler as! TestCounter
|
||||
let testCounter = try metrics.expectCounter(counter)
|
||||
XCTAssertEqual(testCounter.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(testCounter.values.first!.1, Int64(value), "expected value to match")
|
||||
XCTAssertEqual(testCounter.values.first, Int64(value), "expected value to match")
|
||||
XCTAssertEqual(metrics.counters.count, 1, "counter should have been stored")
|
||||
|
||||
let identity = ObjectIdentifier(counter)
|
||||
|
|
@ -355,27 +772,31 @@ class MetricsTests: XCTestCase {
|
|||
let counterAgain = Counter(label: name)
|
||||
counterAgain.increment(by: value)
|
||||
|
||||
let testCounterAgain = counterAgain.handler as! TestCounter
|
||||
let testCounterAgain = try metrics.expectCounter(counterAgain)
|
||||
XCTAssertEqual(testCounterAgain.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(testCounterAgain.values.first!.1, Int64(value), "expected value to match")
|
||||
XCTAssertEqual(testCounterAgain.values.first, 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-\(NSUUID().uuidString)"
|
||||
let value = Int64.random(in: 0 ... 1000)
|
||||
let name = "timer-\(UUID().uuidString)"
|
||||
let value = Int64.random(in: 0...1000)
|
||||
|
||||
let timer = Timer(label: name)
|
||||
timer.recordNanoseconds(value)
|
||||
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(testTimer.values.first!.1, value, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values.first, value, "expected value to match")
|
||||
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
|
||||
|
||||
let identity = ObjectIdentifier(timer)
|
||||
|
|
@ -384,11 +805,35 @@ class MetricsTests: XCTestCase {
|
|||
|
||||
let timerAgain = Timer(label: name)
|
||||
timerAgain.recordNanoseconds(value)
|
||||
let testTimerAgain = timerAgain.handler as! TestTimer
|
||||
let testTimerAgain = try metrics.expectTimer(timerAgain)
|
||||
XCTAssertEqual(testTimerAgain.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(testTimerAgain.values.first!.1, value, "expected value to match")
|
||||
XCTAssertEqual(testTimerAgain.values.first, 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")
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,11 @@
|
|||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import MetricsTestKit
|
||||
import XCTest
|
||||
|
||||
@testable import CoreMetrics
|
||||
@testable import Metrics
|
||||
import XCTest
|
||||
|
||||
class MetricsExtensionsTests: XCTestCase {
|
||||
func testTimerBlock() throws {
|
||||
|
|
@ -22,14 +24,14 @@ class MetricsExtensionsTests: XCTestCase {
|
|||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let name = "timer-\(NSUUID().uuidString)"
|
||||
let name = "timer-\(UUID().uuidString)"
|
||||
let delay = 0.05
|
||||
Timer.measure(label: name) {
|
||||
Thread.sleep(forTimeInterval: delay)
|
||||
}
|
||||
let timer = metrics.timers[name] as! TestTimer
|
||||
let timer = try metrics.expectTimer(name)
|
||||
XCTAssertEqual(1, timer.values.count, "expected number of entries to match")
|
||||
XCTAssertGreaterThan(timer.values[0].1, Int64(delay * 1_000_000_000), "expected delay to match")
|
||||
XCTAssertGreaterThan(timer.values[0], Int64(delay * 1_000_000_000), "expected delay to match")
|
||||
}
|
||||
|
||||
func testTimerWithTimeInterval() throws {
|
||||
|
|
@ -38,11 +40,11 @@ class MetricsExtensionsTests: XCTestCase {
|
|||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let timer = Timer(label: "test-timer")
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let timeInterval = TimeInterval(Double.random(in: 1 ... 500))
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
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].1, Int64(timeInterval * 1_000_000_000), "expected value to match")
|
||||
XCTAssertEqual(testTimer.values[0], Int64(timeInterval * 1_000_000_000), "expected value to match")
|
||||
}
|
||||
|
||||
func testTimerWithDispatchTime() throws {
|
||||
|
|
@ -51,95 +53,226 @@ class MetricsExtensionsTests: XCTestCase {
|
|||
MetricsSystem.bootstrapInternal(metrics)
|
||||
// run the test
|
||||
let timer = Timer(label: "test-timer")
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
// 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].1), nano.nano(), "expected value to match")
|
||||
XCTAssertEqual(Int(testTimer.values[0]), 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].1), micro.nano(), "expected value to match")
|
||||
XCTAssertEqual(Int(testTimer.values[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].1), milli.nano(), "expected value to match")
|
||||
XCTAssertEqual(Int(testTimer.values[2]), 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].1), sec.nano(), "expected value to match")
|
||||
XCTAssertEqual(Int(testTimer.values[3]), 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].1, 0, "expected value 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")
|
||||
}
|
||||
|
||||
func testTimerUnits() throws {
|
||||
let metrics = TestMetrics()
|
||||
MetricsSystem.bootstrapInternal(metrics)
|
||||
|
||||
let name = "timer-\(NSUUID().uuidString)"
|
||||
let value = Int64.random(in: 0 ... 1000)
|
||||
let name = "timer-\(UUID().uuidString)"
|
||||
let value = Int64.random(in: 0...1000)
|
||||
|
||||
let timer = Timer(label: name)
|
||||
timer.recordNanoseconds(value)
|
||||
|
||||
let testTimer = timer.handler as! TestTimer
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
XCTAssertEqual(testTimer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(testTimer.values.first!.1, value, "expected value to match")
|
||||
XCTAssertEqual(testTimer.values.first, value, "expected value to match")
|
||||
XCTAssertEqual(metrics.timers.count, 1, "timer should have been stored")
|
||||
|
||||
let secondsName = "timer-seconds-\(NSUUID().uuidString)"
|
||||
let secondsValue = Int64.random(in: 0 ... 1000)
|
||||
let secondsName = "timer-seconds-\(UUID().uuidString)"
|
||||
let secondsValue = Int64.random(in: 0...1000)
|
||||
let secondsTimer = Timer(label: secondsName, preferredDisplayUnit: .seconds)
|
||||
secondsTimer.recordSeconds(secondsValue)
|
||||
|
||||
let testSecondsTimer = secondsTimer.handler as! TestTimer
|
||||
let testSecondsTimer = try metrics.expectTimer(secondsTimer)
|
||||
XCTAssertEqual(testSecondsTimer.values.count, 1, "expected number of entries to match")
|
||||
XCTAssertEqual(metrics.timers.count, 2, "timer should have been stored")
|
||||
}
|
||||
|
||||
func testPreferDisplayUnit() {
|
||||
func testPreferDisplayUnit() throws {
|
||||
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 = timer.handler as! TestTimer
|
||||
let testTimer = try metrics.expectTimer(timer)
|
||||
|
||||
testTimer.preferDisplayUnit(.nanoseconds)
|
||||
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000 * 1000, accuracy: 1.0, "expected value to match")
|
||||
XCTAssertEqual(
|
||||
testTimer.valueInPreferredUnit(atIndex: 0),
|
||||
value * 1000 * 1000 * 1000,
|
||||
accuracy: 1.0,
|
||||
"expected value to match"
|
||||
)
|
||||
|
||||
testTimer.preferDisplayUnit(.microseconds)
|
||||
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000 * 1000, accuracy: 0.001, "expected value to match")
|
||||
XCTAssertEqual(
|
||||
testTimer.valueInPreferredUnit(atIndex: 0),
|
||||
value * 1000 * 1000,
|
||||
accuracy: 0.001,
|
||||
"expected value to match"
|
||||
)
|
||||
|
||||
testTimer.preferDisplayUnit(.milliseconds)
|
||||
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value * 1000, accuracy: 0.000001, "expected value to match")
|
||||
XCTAssertEqual(
|
||||
testTimer.valueInPreferredUnit(atIndex: 0),
|
||||
value * 1000,
|
||||
accuracy: 0.000001,
|
||||
"expected value to match"
|
||||
)
|
||||
|
||||
testTimer.preferDisplayUnit(.seconds)
|
||||
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value, accuracy: 0.000000001, "expected value to match")
|
||||
XCTAssertEqual(
|
||||
testTimer.valueInPreferredUnit(atIndex: 0),
|
||||
value,
|
||||
accuracy: 0.000000001,
|
||||
"expected value to match"
|
||||
)
|
||||
|
||||
testTimer.preferDisplayUnit(.minutes)
|
||||
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / 60, accuracy: 0.000000001, "expected value to match")
|
||||
XCTAssertEqual(
|
||||
testTimer.valueInPreferredUnit(atIndex: 0),
|
||||
value / 60,
|
||||
accuracy: 0.000000001,
|
||||
"expected value to match"
|
||||
)
|
||||
|
||||
testTimer.preferDisplayUnit(.hours)
|
||||
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60), accuracy: 0.000000001, "expected value to match")
|
||||
XCTAssertEqual(
|
||||
testTimer.valueInPreferredUnit(atIndex: 0),
|
||||
value / (60 * 60),
|
||||
accuracy: 0.000000001,
|
||||
"expected value to match"
|
||||
)
|
||||
|
||||
testTimer.preferDisplayUnit(.days)
|
||||
XCTAssertEqual(testTimer.retriveValueInPreferredUnit(atIndex: 0), value / (60 * 60 * 24), accuracy: 0.000000001, "expected value to match")
|
||||
XCTAssertEqual(
|
||||
testTimer.valueInPreferredUnit(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 {
|
||||
switch self {
|
||||
// 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 {
|
||||
case .nanoseconds(let value):
|
||||
return value
|
||||
case .microseconds(let value):
|
||||
|
|
@ -150,6 +283,30 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
#!/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"
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
#!/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"
|
||||
Loading…
Reference in New Issue