Skip to content

Commit

Permalink
feat: API client and Xunit plugin.
Browse files Browse the repository at this point in the history
  • Loading branch information
jimsynz committed Jul 27, 2022
1 parent 78cbf80 commit 9edd96a
Show file tree
Hide file tree
Showing 23 changed files with 1,257 additions and 134 deletions.
11 changes: 11 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/ubuntu/.devcontainer/base.Dockerfile

# [Choice] Ubuntu version (use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon): ubuntu-22.04, ubuntu-20.04, ubuntu-18.04
ARG VARIANT="jammy"
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

RUN ln -s /usr/share/dotnet /usr/local/dotnet
31 changes: 31 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/ubuntu
{
"name": "Ubuntu",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick an Ubuntu version: jammy / ubuntu-22.04, focal / ubuntu-20.04, bionic /ubuntu-18.04
// Use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon.
"args": { "VARIANT": "ubuntu-20.04" }
},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "uname -a",

// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
"features": {
"dotnet": "6.0"
},
"customizations": {
"vscode": {
"extensions": [
"Ionide.Ionide-fsharp",
"ms-dotnettools.csharp"
]
}
}
}
2 changes: 2 additions & 0 deletions BuildkiteTestAnalytics/.gitignore → .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
bin
obj

.fake
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.inlayHints.enabled": "offUnlessPressed"
}
33 changes: 33 additions & 0 deletions BuildkiteTestAnalytics.Tests/BuildkiteTestAnalytics.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="PayloadTests.fs" />
<Compile Include="TracingTests.fs" />
<Compile Include="TestDataTests.fs" />
<Compile Include="TimingTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BuildkiteTestAnalytics\BuildkiteTestAnalytics.fsproj" />
<ProjectReference Include="..\BuildkiteTestAnalytics.XUnitCollector\BuildkiteTestAnalytics.XUnitCollector.fsproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
205 changes: 205 additions & 0 deletions BuildkiteTestAnalytics.Tests/PayloadTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
module PayloadTests

open System
open System.Collections.Generic
open Xunit
open BuildkiteTestAnalytics

let getEnvVarFactory (env: Map<string, string>) : (string -> string) =
(fun key ->
let maybe = env |> Map.tryFind key
Option.defaultValue "" maybe)

let rand = Random()

[<Fact>]
let ``when it cannot detect the environment it returns none`` () =
let getEnvVar = getEnvVarFactory (Map [])
let payload = Payload.Init(Some getEnvVar)
Assert.Same(payload, None)

[<Fact>]
let ``when it detects a Buildkite CI environment it returns an empty payload`` () =
let buildId = Guid.NewGuid.ToString()

let env =
Map [ ("BUILDKITE_BUILD_ID", buildId)
("BUILDKITE_BUILD_URL", sprintf "https://example.test/buildkite/%s" buildId)
("BUILDKITE_BRANCH", "feat/add-mr-fusion-to-delorean")
("BUILDKITE_COMMIT", buildId)
("BUILDKITE_BUILD_NUMBER", rand.Next(999).ToString())
("BUILDKITE_JOB_ID", rand.Next(999).ToString())
("BUILDKITE_MESSAGE",
"Silence, Earthling! My Name Is Darth Vader. I Am An Extraterrestrial From The Planet Vulcan!") ]

let payload = Payload.Init(Some(getEnvVarFactory env))
Assert.True(Option.isSome payload)

let payload = payload.Value
let now = Timing.now ()
Assert.InRange(payload.StartedAt, now - 20, now + 20)
Assert.Empty(payload.Data)

let runEnv = payload.RuntimeEnvironment
Assert.Equal(runEnv.Ci, "buildkite")
Assert.Equal(runEnv.Key, env.["BUILDKITE_BUILD_ID"])
Assert.Equal(runEnv.Number, Some(env.["BUILDKITE_BUILD_NUMBER"]))
Assert.Equal(runEnv.JobId, Some(env.["BUILDKITE_JOB_ID"]))
Assert.Equal(runEnv.Branch, Some(env.["BUILDKITE_BRANCH"]))
Assert.Equal(runEnv.CommitSha, Some(env.["BUILDKITE_COMMIT"]))
Assert.Equal(runEnv.Message, Some(env.["BUILDKITE_MESSAGE"]))
Assert.Equal(runEnv.Url, Some(env.["BUILDKITE_BUILD_URL"]))


[<Fact>]
let ``when it detects a Github Actions CI environment it returns an empty payload`` () =
let env =
Map [ ("GITHUB_ACTION", "__doc-brown_grandfather-paradox_flux-capacitor")
("GITHUB_RUN_NUMBER", rand.Next(999).ToString())
("GITHUB_RUN_ATTEMPT", rand.Next(999).ToString())
("GITHUB_REPOSITORY", "doc-brown/flux-capacitor")
("GITHUB_REF", "feat/add-time-circuits")
("GITHUB_RUN_ID", Guid.NewGuid.ToString())
("GITHUB_SHA", Guid.NewGuid.ToString()) ]

let payload = Payload.Init(Some(getEnvVarFactory env))
Assert.True(Option.isSome payload)

let payload = payload.Value
let now = Timing.now ()
Assert.InRange(payload.StartedAt, now - 20, now + 20)
Assert.Empty(payload.Data)

let runEnv = payload.RuntimeEnvironment
Assert.Equal(runEnv.Ci, "github_actions")

Assert.Equal(
runEnv.Key,
sprintf "%s-%s-%s" env.["GITHUB_ACTION"] env.["GITHUB_RUN_NUMBER"] env.["GITHUB_RUN_ATTEMPT"]
)

Assert.Equal(runEnv.Number, Some(env.["GITHUB_RUN_NUMBER"]))
Assert.Same(runEnv.JobId, None)
Assert.Equal(runEnv.Branch, Some(env.["GITHUB_REF"]))
Assert.Equal(runEnv.CommitSha, Some(env.["GITHUB_SHA"]))
Assert.Equal(runEnv.Message, None)

Assert.Equal(
runEnv.Url,
Some(sprintf "https://github.com/doc-brown/flux-capacitor/actions/run/%s" env.["GITHUB_RUN_ID"])
)

[<Fact>]
let ``when it detects a Circle CI environment it returns an empty payload`` () =
let env =
Map [ ("CIRCLE_BUILD_NUM", rand.Next(999).ToString())
("CIRCLE_WORKFLOW_ID", Guid.NewGuid.ToString())
("CIRCLE_BUILD_URL", "https://example.test/circle")
("CIRCLE_BRANCH", "rufus")
("CIRCLE_SHA1", Guid.NewGuid.ToString()) ]

let payload = Payload.Init(Some(getEnvVarFactory env))
Assert.True(Option.isSome payload)

let payload = payload.Value
let now = Timing.now ()
Assert.InRange(payload.StartedAt, now - 20, now + 20)
Assert.Empty(payload.Data)

let runEnv = payload.RuntimeEnvironment
Assert.Equal(runEnv.Ci, "circleci")
Assert.Equal(runEnv.Key, sprintf "%s-%s" env.["CIRCLE_WORKFLOW_ID"] env.["CIRCLE_BUILD_NUM"])
Assert.Equal(runEnv.Number, Some(env.["CIRCLE_BUILD_NUM"]))
Assert.Same(runEnv.JobId, None)
Assert.Equal(runEnv.Branch, Some(env.["CIRCLE_BRANCH"]))
Assert.Equal(runEnv.CommitSha, Some(env.["CIRCLE_SHA1"]))
Assert.Equal(runEnv.Message, None)
Assert.Equal(runEnv.Url, Some "https://example.test/circle")

[<Fact>]
let ``when it detects a generic CI environment it returns an empty payload`` () =
let env = Map [ ("CI", "true") ]

let payload = Payload.Init(Some(getEnvVarFactory env))
Assert.True(Option.isSome payload)

let payload = payload.Value
let now = Timing.now ()
Assert.InRange(payload.StartedAt, now - 20, now + 20)
Assert.Empty(payload.Data)

let runEnv = payload.RuntimeEnvironment
Assert.Equal(runEnv.Ci, "generic")
Guid.Parse(runEnv.Key) |> ignore
Assert.Same(runEnv.Number, None)
Assert.Same(runEnv.JobId, None)
Assert.Same(runEnv.Branch, None)
Assert.Same(runEnv.CommitSha, None)
Assert.Same(runEnv.Message, None)
Assert.Same(runEnv.Url, None)

let genericEmptyPayload () =
let env = Map [ ("CI", "true") ]
Payload.Init(Some(getEnvVarFactory env)).Value

let fakeTest () =
TestData.Init(Guid.NewGuid.ToString(), Some("scope"), Some("name"), "identifier", Some "location", Some "fileName")

[<Fact>]
let ``AddTestResult adds a TestResult.Test to the Collection`` () =
let payload = genericEmptyPayload ()

Assert.Empty(payload.Data)

let test = fakeTest()

let payload = Payload.AddTestResult(payload, test)

Assert.NotEmpty(payload.Data)

Assert.Equal(test, payload.Data.Head)

[<Fact>]
let ``when the payload has fewer tests than the batch size IntoBatches returns the original payload`` () =
let payload = Payload.AddTestResult(genericEmptyPayload(), fakeTest())
let payloads = Payload.IntoBatches(payload, 10)
Assert.Equal(payloads.Length, 1)
Assert.Equal(payload, payloads.Head)

let rec payloadStuffer(payload: Payload.Payload, remaining: int) : Payload.Payload =
if remaining = 0 then
payload
else
let payload = Payload.AddTestResult(payload, fakeTest())
payloadStuffer(payload, remaining - 1)


[<Fact>]
let ``when the payload has more tests than the batch size IntoBatches returns multiple payloads`` () =
let payload = genericEmptyPayload()
let payload = payloadStuffer(payload, 95)
let payloads = Payload.IntoBatches(payload, 10)

Assert.Equal(payloads.Length, 10)

let first_payload = payloads.Head
let last_payload = List.last payloads

Assert.Equal(first_payload.Data.Length, 10)
Assert.Equal(last_payload.Data.Length, 5)

[<Fact>]
let ``AsJson converts the payload into a map`` () =
let payload = genericEmptyPayload()
let payload = payloadStuffer(payload, 3)
let json = Payload.AsJson(payload)

Assert.Equal(json["format"], "json")
Assert.Contains("run_env", json.Keys)
Assert.Contains("data", json.Keys)

let env = json["run_env"] :?> Map<string, obj>
Assert.Equal(env["CI"], "generic")

let data = json["data"] :?> Map<string, obj> list
Assert.Equal(data.Length, 3)
1 change: 1 addition & 0 deletions BuildkiteTestAnalytics.Tests/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module Program = let [<EntryPoint>] main _ = 0
99 changes: 99 additions & 0 deletions BuildkiteTestAnalytics.Tests/TestDataTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
module TestDataTests

open System
open Xunit
open BuildkiteTestAnalytics

let rand = Random()

let fakeTest () =
TestData.Init(Guid.NewGuid.ToString(), Some("scope"), Some("name"), "identifier", Some "location", Some "fileName")

let fakeSpan () =
Tracing.Init(Tracing.Section.Sql, rand.Next(1000), None, None, None)

[<Fact>]
let ``Init creates a new test`` () =
let id = Guid.NewGuid().ToString()
let scope = "BuildkiteTestAnalytics.Tests"
let name = "Init creates a new test"
let identifier = "BuildkiteTestAnalytics.TestDataTests.Init creates a new test"
let location = "BuildkiteTestAnalytics.TestDataTests/TestDataTests.fs: line 8"
let fileName = "TestDataTests.fs"

let testData =
TestData.Init(id, Some(scope), Some(name), identifier, Some(location), Some(fileName))

let now = Timing.now ()

Assert.Equal(testData.Id, id)
Assert.Equal(testData.Scope, Some(scope))
Assert.Equal(testData.Name, Some(name))
Assert.Equal(testData.Identifier, identifier)
Assert.Equal(testData.Location, Some(location))
Assert.Equal(testData.FileName, Some(fileName))
Assert.InRange(testData.StartAt, now - 5, now + 5)
Assert.Equal(testData.EndAt, None)
Assert.Equal(testData.Duration, None)
Assert.Empty(testData.Children)
Assert.Same(testData.Result, TestData.TestResult.Unknown)


[<Fact>]
let ``AddSpan adds a span to a test`` () =
let test = fakeTest ()
let span = fakeSpan ()
let test = TestData.AddSpan(test, span)
Assert.NotEmpty(test.Children)

[<Fact>]
let ``Passed marks the test as passed`` () =
let test = fakeTest ()
let test = TestData.Passed(test)

Assert.Same(test.Result, TestData.TestResult.Passed)

[<Fact>]
let ``Failed marks the test as failed`` () =
let test = fakeTest ()
let test = TestData.Failed(test, None)

Assert.Equal(test.Result, TestData.TestResult.Failed None)

[<Fact>]
let ``AsJson converts the test into a dict`` () =
let now = Timing.now ()
let test = fakeTest ()
let test = TestData.AddSpan(test, fakeSpan ())
let json = TestData.AsJson(test, now)

Assert.Equal(json["id"], test.Id)
Assert.Equal(json["scope"], test.Scope.Value)
Assert.Equal(json["name"], test.Name.Value)
Assert.Equal(json["identifier"], test.Identifier)
Assert.Equal(json["result"], "unknown")

let test = TestData.Passed(test)
let json = TestData.AsJson(test, now)
Assert.Equal(json["result"], "passed")

let test = TestData.Failed(test, None)
let json = TestData.AsJson(test, now)
Assert.Equal(json["result"], "failed")
Assert.DoesNotContain(json.Keys, (fun key -> key = "failure_reason"))

let test = TestData.Failed(test, Some("a perfectly reasonable reason to fail"))
let json = TestData.AsJson(test, now)
Assert.Equal(json["result"], "failed")
Assert.Equal(json["failure_reason"], "a perfectly reasonable reason to fail")

let history = json["history"] :?> Map<string, obj>
Assert.Equal(history["section"], "top")
Assert.Equal(history["start_at"], Timing.elapsedSeconds (test.StartAt, now))
Assert.Equal(history["end_at"], Timing.elapsedSeconds (test.EndAt.Value, now))
Assert.Equal(history["duration"], Timing.elapsedSeconds (test.EndAt.Value, test.StartAt))

let test = TestData.WithDuration(test, 32000)
let json = TestData.AsJson(test, now)
let history = json["history"] :?> Map<string, obj>
Assert.Equal(history["duration"], 32.0)
Loading

0 comments on commit 9edd96a

Please sign in to comment.