diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 942ede7..9cc6930 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,6 +7,7 @@ permissions: env: CONSOLE_OUTPUT: XTerm + DD_API_KEY: ${{secrets.DD_API_KEY}} jobs: test: @@ -45,6 +46,16 @@ jobs: ruby-version: ${{matrix.ruby}} bundler-cache: true + - name: Setup docker + if: runner.os == 'macos' + run: | + brew install docker + colima start + + - uses: datadog/agent-github-action@v1.3 + with: + api_key: ${{secrets.DD_API_KEY}} + - name: Run tests timeout-minutes: 10 run: bundle exec bake test diff --git a/bin/sus-turbo b/bin/sus-turbo new file mode 100755 index 0000000..1dc5d79 --- /dev/null +++ b/bin/sus-turbo @@ -0,0 +1,137 @@ +#!/usr/bin/env ruby + +class Worker + PATH = File.expand_path('sus-worker', __dir__) + + def self.fork + worker = self.new + worker.fork + + return worker unless block_given? + + begin + yield worker + ensure + worker.close + end + + return worker + end + + def initialize + @input = IO.pipe + @output = IO.pipe + @pid = nil + end + + def fork + unless @pid + @pid = Process.spawn(PATH, in: @input[0], out: @output[1]) + @input[0].close + @output[1].close + end + + return self + end + + def close + if @pid + Process.kill(:TERM, @pid) + end + end + + def join + if @pid + status = Process.wait(@pid) + + @pid = nil + + return status + end + end + + def call(job) + assertions = Sus::Assertions.new(output: Sus::Output::Null.new) + + @input.last.puts JSON.generate({run: [job.identity.to_s]}) + + while line = @output.first.gets + message = JSON.parse(line) + + if message[] + end + + return assertions + end +end + +require 'etc' +count = Etc.nprocessors + +require_relative '../lib/sus/config' +config = Sus::Config.load + +Result = Struct.new(:job, :assertions) + +require_relative '../lib/sus' +require_relative '../lib/sus/output' + +jobs = Thread::Queue.new +results = Thread::Queue.new +guard = Thread::Mutex.new +progress = Sus::Output::Progress.new(config.output) + +loader = Thread.new do + registry = config.registry + + registry.each do |child| + guard.synchronize{progress.expand} + jobs << child + end + + jobs.close +end + +top = Sus::Assertions.new(output: Sus::Output::Null.new) +config.before_tests(top) + +aggregation = Thread.new do + while result = results.pop + guard.synchronize{progress.increment} + + top.add(result.assertions) + + guard.synchronize{progress.report(count, top, :busy)} + end + + guard.synchronize{progress.clear} + + top +end + +workers = count.times.map do |index| + Thread.new do + Worker.fork do |worker| + while job = jobs.pop + guard.synchronize{progress.report(index, job, :busy)} + + assertions = worker.call(job) + results << Result.new(job, assertions) + + guard.synchronize{progress.report(index, "idle", :free)} + end + end + end +end + +loader.join + +workers.each(&:join) +results.close + +assertions = aggregation.value +config.after_tests(assertions) + +unless assertions.passed? + exit(1) +end diff --git a/bin/sus-worker b/bin/sus-worker new file mode 100755 index 0000000..f4a3ced --- /dev/null +++ b/bin/sus-worker @@ -0,0 +1,63 @@ +#!/usr/bin/env ruby + +require 'json' + +require_relative '../lib/sus/config' +config = Sus::Config.load + +require_relative '../lib/sus' +require_relative '../lib/sus/output/structured' + +verbose = false +$stdout.sync = true + +input = $stdin.dup +$stdin.reopen(File::NULL) +output = $stdout.dup +$stdout.reopen($stderr) + +while line = input.gets + message = JSON.parse(line) + + if test = message['run'] + top = Sus::Assertions.new(measure: true) + config.before_tests(top) + + loader = Thread.new do + registry = config.load_registry(tests) + + registry.each do |child| + jobs << child + end + + jobs.close + end + + while job = jobs.pop + top.nested do |assertions| + job.call(assertions) + end + end + + loader.join + workers.each(&:join) + results.close + + aggregate.join + config.after_tests(top) + + workers.each(&:join) + + if config.respond_to?(:covered) + if covered = config.covered and covered.record? + covered.policy.each do |coverage| + output.puts JSON.generate({coverage: coverage.path, counts: coverage.counts}) + end + end + end + + output.puts JSON.generate({finished: true, message: top.output.string, duration: top.clock.ms}) + else + $stderr.puts "Unknown message: #{message}" + end +end diff --git a/config/sus.rb b/config/sus.rb index 9905469..83420fe 100644 --- a/config/sus.rb +++ b/config/sus.rb @@ -5,3 +5,8 @@ require 'covered/sus' include Covered::Sus + +if ENV['DD_API_KEY'] + require 'sus/integrations/datadog' + include Sus::Integrations::Datadog +end diff --git a/gems.rb b/gems.rb index 592cbe0..bcba868 100644 --- a/gems.rb +++ b/gems.rb @@ -19,3 +19,5 @@ gem "bake-test-external" gem "covered" end + +gem "ddtrace" diff --git a/lib/sus/context.rb b/lib/sus/context.rb index 7600dc4..a3a1558 100644 --- a/lib/sus/context.rb +++ b/lib/sus/context.rb @@ -56,6 +56,12 @@ def full_name return output.string end + def full_name + output = Output::Buffered.new + print(output) + return output.string + end + def call(assertions) return if self.empty? diff --git a/lib/sus/integrations/datadog.rb b/lib/sus/integrations/datadog.rb new file mode 100644 index 0000000..85ab667 --- /dev/null +++ b/lib/sus/integrations/datadog.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2022, by Samuel Williams. + +require 'ddtrace' + +module Sus + module Integrations + module Datadog + def before_tests(...) + ::Datadog.configure do |configuration| + # Set the env to test: + configuration.env = 'test' + + # Activate test tracing: + configuration.tracing.enabled = true + + # Configures the tracer to ensure results delivery: + configuration.ci.enabled = true + + # The name of the service or library under test: + configuration.service = 'sus' + + # Disable noisy logging: + configuration.diagnostics.startup_logs.enabled = false + end + + super + end + + def after_tests(...) + super + + ::Datadog.shutdown! + end + + def make_registry + super.tap do |registry| + registry.base.include(Instrumentation) + end + end + + module Instrumentation + def around + options = { + framework: "sus", + framework_version: Sus::VERSION, + test_name: self.class.to_s, + test_suite: self.class.superclass.full_name, + test_type: 'unit' + } + + options[:span_options] = { + resource: "#{options[:test_suite]} #{options[:test_name]}", + } + + ::Datadog::CI::Test.trace("sus.test", options) do |span| + span['test.identity'] = self.class.identity.to_s + + status = nil + + begin + result = super + status = :passed + + if @__assertions__.passed? + ::Datadog::CI::Test.passed!(span) + else + message = @__assertions__.message + ::Datadog::CI::Test.failed!(span, message.text) + end + + return result + rescue => error + status = :failed + ::Datadog::CI::Test.failed!(span, error) + raise + ensure + ::Datadog::CI::Test.skipped!(span) unless status + end + end + end + end + end + end +end