scampi 0.1.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d4ea4ec73208870f1578c9a9692ad391f76e5a40de28ad371d966ebeeeea095
4
- data.tar.gz: e542d8bb09d7d121976f41daa04b1f62609302a81fdd030bd9183525dea18086
3
+ metadata.gz: b1379730cbf012c3a1c4a769f7a231b15843790683b33e937152b79d02407492
4
+ data.tar.gz: b2e0738030762951803c4762fac51b694847d3d88e4b485c6fcfb34686ec8df0
5
5
  SHA512:
6
- metadata.gz: 03c07b2b4e3eea11ab08873bf96467b0bca682f55230ce644451e4628d638bcec91bebd01046d9ca907b5c55bf4ea024b25d0a0aca719f977bacd7b3139e97bd
7
- data.tar.gz: 777988311e98de67d4d340314fe2d4d87980b3fefa275c28d202dc29d37017ad0fe0f42069e26927a5f78ed38415884ecf42868018ca047bd2033b6cced5476e
6
+ metadata.gz: 14fa648479ebfb812588d4e723d039de5f6523b92757ad1a7ffe4006a03f2c72e7c9ed269334f4f70d5dfa4cc53d5f64c0863c13481d27d0542b545a6d8d47ca
7
+ data.tar.gz: 26b7ffec3c42be6eb4b5c6cf4053c692543939465950495b600bc89c1a8a10469a5e6998c96e51c5725d2b1578cd76ec3a35e0b041f5678fcd883b4466742d4e
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [1.0.0] - 2026-07-04
6
+
7
+ ### Changed
8
+ - **Breaking:** co-located tests now live in an `__END__` section instead of a
9
+ `test do ... end` block. The section after `__END__` is never parsed in
10
+ production and is evaluated as spec code by the `scampi` runner (backtraces
11
+ keep the original file/line numbers). `scampi` with no arguments now
12
+ auto-discovers `.rb` files whose `__END__` section contains specs.
13
+
14
+ ### Removed
15
+ - The `Kernel#test` method (`lib/scampi/kernel_ext.rb`). Running a source file
16
+ directly (`ruby greet.rb`) no longer executes its tests; use `scampi` instead.
17
+
18
+ ## [0.1.9] - 2026-07-04
19
+
20
+ ### Changed
21
+ - Replaced the vendored `colorize` sources with a minimal `lib/scampi/colors.rb`
22
+ providing only the `String#green` and `String#red` methods Scampi uses. Output
23
+ is byte-for-byte identical; ~200 lines of vendored code reduced to ~15.
24
+
25
+ ## [0.1.8] - 2026-07-04
26
+
27
+ ### Changed
28
+ - Vendored the `colorize` and `colorize-extended` sources into `lib/`, removing
29
+ the external `colorize-extended` runtime dependency (and its transitive
30
+ `colorize` dependency). Scampi now has no runtime gem dependencies.
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
-
5
- gem "colorize-extended"
data/Gemfile.lock CHANGED
@@ -1,28 +1,21 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- scampi (0.1.6)
5
- colorize-extended
4
+ scampi (0.1.9)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
9
8
  specs:
10
- colorize (1.1.0)
11
- colorize-extended (0.1.0)
12
- colorize (~> 1.1)
13
9
 
14
10
  PLATFORMS
15
11
  ruby
16
12
  x86_64-linux
17
13
 
18
14
  DEPENDENCIES
19
- colorize-extended
20
15
  scampi!
21
16
 
22
17
  CHECKSUMS
23
- colorize (1.1.0) sha256=30b5237f0603f6662ab8d1fc2bd4a96142b806c6415d79e45ef5fdc6a0cfc837
24
- colorize-extended (0.1.0) sha256=e8c39986e41ee2e14623c8fa02cf851ef4b83d2fe1392daa2b4d81f0df7bedc9
25
- scampi (0.1.6)
18
+ scampi (0.1.9)
26
19
 
27
20
  BUNDLED WITH
28
- 4.0.7
21
+ 2.6.9
data/README.md CHANGED
@@ -2,17 +2,25 @@
2
2
 
3
3
  A small Ruby test framework forked from [Bacon](https://github.com/chneukirchen/bacon) with built-in [TAP (Test Anything Protocol)](https://testanything.org/) output.
4
4
 
5
+ ## Requirements
6
+
7
+ - Ruby >= 3.3
8
+ - [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) — used to find test files
9
+
5
10
  ## Usage
6
11
 
7
- Tests can live alongside your code using the `test` blockit only runs when the file is executed directly or via `scampi`:
12
+ Tests can live alongside your code in an `__END__` section — the specs never
13
+ load in production (Ruby stops parsing at `__END__`), and `scampi` picks them up:
8
14
 
9
15
  ```ruby
10
16
  # greet.rb
11
17
  def greet(name) = "hello #{name}"
12
18
 
13
- test do
14
- greeting = proc { |name| greet(name) }
19
+ __END__
15
20
 
21
+ greeting = proc { |name| greet(name) }
22
+
23
+ describe "greet" do
16
24
  it "equality and matching" do
17
25
  greeting.("world").should == "hello world"
18
26
  greeting.("world").should.equal "hello world"
@@ -87,7 +95,36 @@ test do
87
95
  end
88
96
  ```
89
97
 
98
+ Run a file (or let `scampi` auto-discover every `.rb` with an `__END__` spec section):
99
+
90
100
  ```
91
- $ ruby greet.rb
92
101
  $ scampi greet.rb
102
+ $ scampi
103
+ ```
104
+
105
+ ## GitHub Actions
106
+
107
+ ```yaml
108
+ name: Test
109
+
110
+ on: [push, pull_request]
111
+
112
+ concurrency:
113
+ group: ${{ github.workflow }}-${{ github.ref }}
114
+ cancel-in-progress: true
115
+
116
+ jobs:
117
+ test:
118
+ runs-on: ubuntu-latest
119
+ steps:
120
+ - uses: actions/checkout@v4
121
+
122
+ - uses: ruby/setup-ruby@v1
123
+ with:
124
+ ruby-version: "3.3"
125
+ bundler-cache: true
126
+
127
+ - run: sudo apt-get install -y ripgrep
128
+
129
+ - run: bundle exec scampi
93
130
  ```
data/exe/scampi CHANGED
@@ -1,27 +1,27 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'bundler/setup'
4
3
  $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
5
4
  require 'scampi'
6
5
 
7
6
  ENV["TEST"] = "true"
8
7
 
9
8
  files = if ARGV.empty?
10
- `rg -l "^test do$" .`.split("\n")
11
- else
12
- ARGV.flat_map { |pattern| Dir.glob(pattern) }
13
- end
9
+ # Auto-discover files carrying co-located tests in an __END__ section.
10
+ `rg -l "^__END__$" .`.split("\n").select { |f|
11
+ next false unless f.end_with?('.rb')
12
+ tail = File.read(f).split(/^__END__$\n?/, 2)[1]
13
+ tail && tail =~ /^\s*(describe|context|shared|it)\b/
14
+ }
15
+ else
16
+ ARGV.flat_map { |pattern| Dir.glob(pattern) }
17
+ end
14
18
 
15
19
  if files.empty?
16
20
  $stderr.puts "no test files found"
17
21
  exit 1
18
22
  end
19
23
 
20
- files.each { |file|
21
- old_verbose, $-w = $-w, nil
22
- load file
23
- $-w = old_verbose
24
- }
24
+ files.each { |file| Scampi.load_test_file(file) }
25
25
 
26
26
  Scampi.run
27
27
  exit(Scampi::Counter[:errors] + Scampi::Counter[:failed] > 0 ? 1 : 0)
data/flake.nix CHANGED
@@ -10,31 +10,20 @@
10
10
  let
11
11
  pkgs = nixpkgs.legacyPackages.${system};
12
12
  ruby = pkgs.ruby_3_4; # Specify version
13
- kubectlWithKubeconfig = pkgs.writeShellScriptBin "kubectl" ''
14
- #!${pkgs.bash}/bin/bash
15
- KUBECONFIG="$PWD/kubeconfig.yaml" ${pkgs.kubectl}/bin/kubectl "$@"
16
- '';
17
13
  in
18
14
  {
19
15
  devShells.default = pkgs.mkShell {
20
- nativeBuildInputs = [
21
- pkgs.pkg-config # native extension discovery
22
- ];
23
-
24
- buildInputs = [
25
- ruby
26
- pkgs.libyaml # psych gem
27
- pkgs.openssl # openssl gem
28
- kubectlWithKubeconfig
29
- ];
16
+ buildInputs = [ ruby ];
30
17
 
31
18
  shellHook = ''
32
- export GEM_HOME="$PWD/.gem"
19
+ export GEM_HOME="$HOME/.gem"
33
20
  export GEM_PATH="$GEM_HOME"
21
+
34
22
  export PATH="$GEM_HOME/bin:$PATH"
23
+
24
+ export BUNDLE_PATH="$PWD/Gemfile"
35
25
  export BUNDLE_PATH="$GEM_HOME"
36
26
  export BUNDLE_BIN="$GEM_HOME/bin"
37
- export KUBECONFIG="$PWD/kubeconfig.yaml"
38
27
  '';
39
28
  };
40
29
  }
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Minimal ANSI coloring — trimmed from the colorize gem to only the two
4
+ # colors Scampi actually uses (green for "ok", red for "not ok"). Output is
5
+ # byte-for-byte identical to colorize's String#green / String#red.
6
+ module Scampi
7
+ module Colors
8
+ # foreground code, default background (49), default mode (0)
9
+ def green = "\e[0;32;49m#{self}\e[0m"
10
+ def red = "\e[0;31;49m#{self}\e[0m"
11
+ end
12
+ end
13
+
14
+ class String
15
+ include Scampi::Colors
16
+ end
@@ -1,7 +1,19 @@
1
1
  module Scampi
2
+ # A test context created by `describe`. Holds specs, hooks, and child contexts.
3
+ #
4
+ # Operates in two phases:
5
+ # 1. **Register** -- evaluates the block to discover `it` specs and nested children.
6
+ # 2. **Execute** -- runs all registered specs in order, emitting TAP subtests.
2
7
  class Context
3
- attr_reader :name, :block
8
+ # @attribute [String] The context name (passed to `describe`).
9
+ attr_reader :name
4
10
 
11
+ # @attribute [Proc] The block that defines this context's specs and children.
12
+ attr_reader :block
13
+
14
+ # Create a new context.
15
+ #
16
+ # @parameter name [String] The describe block's label.
5
17
  def initialize(name, &block)
6
18
  @name = name
7
19
  @block = block
@@ -11,7 +23,7 @@ module Scampi
11
23
  end
12
24
 
13
25
  # Phase 1: evaluate block to discover specs and children.
14
- # Nothing is executed it and describe just queue.
26
+ # Nothing is executed -- `it` and `describe` just queue items.
15
27
  def register
16
28
  tap do
17
29
  if name =~ RestrictContext
@@ -22,7 +34,9 @@ module Scampi
22
34
  end
23
35
  end
24
36
 
25
- # Count total specs recursively.
37
+ # Count total specs recursively across this context and its children.
38
+ #
39
+ # @returns [Integer]
26
40
  def count
27
41
  @items.sum { |item|
28
42
  case item[0]
@@ -34,8 +48,9 @@ module Scampi
34
48
  end
35
49
 
36
50
  # Phase 2: run all registered specs in order, emitting TAP subtests.
37
- # +indent+ is the nesting depth (0 = inside a top-level describe).
38
- # Returns true if all specs/children passed, false otherwise.
51
+ #
52
+ # @parameter indent [Integer] Nesting depth (0 = inside a top-level describe).
53
+ # @returns [Boolean] Whether all specs and children passed.
39
54
  def execute(indent = 0)
40
55
  prefix = " " * indent
41
56
  inner = " " * (indent + 1)
@@ -76,19 +91,31 @@ module Scampi
76
91
  all_passed
77
92
  end
78
93
 
94
+ # Register a before hook that runs before each spec in this context.
79
95
  def before(&block); @items << [:before, block]; @before << block; end
96
+
97
+ # Register an after hook that runs after each spec in this context.
80
98
  def after(&block); @items << [:after, block]; @after << block; end
81
99
 
100
+ # Include shared context blocks by name.
101
+ #
102
+ # @parameter names [Array(String)] Names of shared contexts to include.
82
103
  def behaves_like(*names)
83
104
  names.each { |name| instance_eval(&Shared[name]) }
84
105
  end
85
106
 
107
+ # Define a spec within this context.
108
+ #
109
+ # @parameter description [String] What this spec asserts.
86
110
  def it(description, &block)
87
111
  return unless description =~ RestrictName
88
112
  block ||= proc { should.flunk "not implemented" }
89
113
  @items << [:spec, description, block]
90
114
  end
91
115
 
116
+ # When called at the context level (outside a spec body), acts as a
117
+ # shortcut for `it('should ...')`. Inside a spec body, delegates to
118
+ # the standard `Object#should`.
92
119
  def should(*args, &block)
93
120
  if Counter[:depth] == 0
94
121
  it('should ' + args.first, &block)
@@ -97,6 +124,15 @@ module Scampi
97
124
  end
98
125
  end
99
126
 
127
+ # Run a single spec with before/after hooks and emit TAP output.
128
+ #
129
+ # @parameter description [String] Spec description.
130
+ # @parameter spec [Proc] The spec body.
131
+ # @parameter befores [Array(Proc)] Before hooks to run.
132
+ # @parameter afters [Array(Proc)] After hooks to run.
133
+ # @parameter indent [Integer] TAP indentation depth.
134
+ # @parameter local_n [Integer] Spec number within this context.
135
+ # @returns [Boolean] Whether the spec passed.
100
136
  def run_requirement(description, spec, befores, afters, indent = 0, local_n = 1)
101
137
  Scampi.handle_requirement(description, indent, local_n) do
102
138
  begin
@@ -145,6 +181,10 @@ module Scampi
145
181
  end
146
182
  end
147
183
 
184
+ # Create a nested child context (TAP subtest).
185
+ #
186
+ # Methods defined on the parent context are copied to the child so
187
+ # helper methods remain accessible.
148
188
  def describe(*args, &block)
149
189
  context = Scampi::Context.new(args.join(' '), &block)
150
190
  (parent_context = self).methods(false).each { |e|
@@ -159,8 +199,13 @@ module Scampi
159
199
  context
160
200
  end
161
201
 
202
+ # Assert that the block raises an exception.
162
203
  def raise?(*args, &block) = block.raise?(*args)
204
+
205
+ # Assert that the block throws a symbol.
163
206
  def throw?(*args, &block) = block.throw?(*args)
207
+
208
+ # Assert that the block changes the result of an expression.
164
209
  def change?(&block) = lambda{}.change?(&block)
165
210
  end
166
211
  end
data/lib/scampi/error.rb CHANGED
@@ -1,7 +1,13 @@
1
1
  module Scampi
2
+ # Custom exception used internally to signal spec failures and missing
3
+ # assertions. The `count_as` attribute determines whether the error
4
+ # increments the `:failed` or `:missing` counter.
2
5
  class Error < RuntimeError
6
+ # @attribute [Symbol] Either `:failed` or `:missing`.
3
7
  attr_accessor :count_as
4
8
 
9
+ # @parameter count_as [Symbol] What counter to increment (`:failed` or `:missing`).
10
+ # @parameter message [String] The error message.
5
11
  def initialize(count_as, message)
6
12
  @count_as = count_as
7
13
  super message
@@ -1,17 +1,27 @@
1
+ # Predicate extensions for truth testing.
1
2
  class Object
3
+ # @returns [Boolean] Always false for non-boolean objects.
2
4
  def true? = false
5
+
6
+ # @returns [Boolean] Always false for non-boolean objects.
3
7
  def false? = false
4
8
  end
5
9
 
6
10
  class TrueClass
11
+ # @returns [Boolean] True.
7
12
  def true? = true
8
13
  end
9
14
 
10
15
  class FalseClass
16
+ # @returns [Boolean] True.
11
17
  def false? = true
12
18
  end
13
19
 
14
20
  class Proc
21
+ # Call the proc and check whether it raises one of the given exceptions.
22
+ #
23
+ # @parameter exceptions [Array(Class)] Exception classes to catch (defaults to RuntimeError).
24
+ # @returns [Exception | Boolean] The caught exception, or false if none was raised.
15
25
  def raise?(*exceptions)
16
26
  call
17
27
  rescue *(exceptions.empty? ? RuntimeError : exceptions) => e
@@ -20,6 +30,10 @@ class Proc
20
30
  false
21
31
  end
22
32
 
33
+ # Call the proc and check whether it throws the given symbol.
34
+ #
35
+ # @parameter sym [Symbol] The symbol to catch.
36
+ # @returns [Boolean]
23
37
  def throw?(sym)
24
38
  catch(sym) {
25
39
  call
@@ -28,6 +42,9 @@ class Proc
28
42
  return true
29
43
  end
30
44
 
45
+ # Call the proc and check whether it changes the result of the given block.
46
+ #
47
+ # @returns [Boolean]
31
48
  def change?
32
49
  pre_result = yield
33
50
  call
@@ -37,12 +54,20 @@ class Proc
37
54
  end
38
55
 
39
56
  class Numeric
57
+ # Check whether this number is within `delta` of `to`.
58
+ #
59
+ # @parameter to [Numeric] The target value.
60
+ # @parameter delta [Numeric] The allowed deviation.
61
+ # @returns [Boolean]
40
62
  def close?(to, delta)
41
63
  (to.to_f - self).abs <= delta.to_f rescue false
42
64
  end
43
65
  end
44
66
 
45
67
  class Object
68
+ # Create a {Scampi::Should} wrapper for this object.
69
+ #
70
+ # @returns [Scampi::Should]
46
71
  def should(*args, &block)
47
72
  Scampi::Should.new(self).be(*args, &block)
48
73
  end
@@ -51,10 +76,14 @@ end
51
76
  module Kernel
52
77
  private
53
78
 
79
+ # Create a top-level test context. Adds a {Scampi::Context} to the global queue.
54
80
  def describe(*args, &block)
55
81
  Scampi.queue << Scampi::Context.new(args.join(' '), &block)
56
82
  end
57
83
 
84
+ # Register a shared context block by name for use with `behaves_like`.
85
+ #
86
+ # @parameter name [String] The shared context name.
58
87
  def shared(name, &block)
59
88
  Scampi::Shared[name] = block
60
89
  end
data/lib/scampi/should.rb CHANGED
@@ -1,14 +1,24 @@
1
1
  module Scampi
2
+ # The assertion wrapper returned by `Object#should`.
3
+ #
4
+ # Supports chainable assertions like `.should.be.empty`, `.should.not == 5`,
5
+ # and `.should.be.a(matcher)`. Undefines predicate and operator methods from
6
+ # Object so they can be intercepted via `method_missing`.
2
7
  class Should
3
- # Kills ==, ===, =~, eql?, equal?, frozen?, instance_of?, is_a?,
4
- # kind_of?, nil?, respond_to?, tainted?
8
+ # Undefine predicate and operator methods so they route through method_missing.
5
9
  instance_methods.each { |name| undef_method name if name =~ /\?|^\W+$/ }
6
10
 
11
+ # Wrap an object for assertion.
12
+ #
13
+ # @parameter object [Object] The value under test.
7
14
  def initialize(object)
8
15
  @object = object
9
16
  @negated = false
10
17
  end
11
18
 
19
+ # Toggle negation. Can be chained: `.should.not.be.empty`.
20
+ #
21
+ # @returns [Should] self, for chaining.
12
22
  def not(*args, &block)
13
23
  @negated = !@negated
14
24
 
@@ -19,6 +29,8 @@ module Scampi
19
29
  end
20
30
  end
21
31
 
32
+ # Identity chain or custom matcher. With no arguments, returns self
33
+ # for further chaining. With a block or lambda, delegates to `satisfy`.
22
34
  def be(*args, &block)
23
35
  if args.empty?
24
36
  self
@@ -33,6 +45,10 @@ module Scampi
33
45
  alias a be
34
46
  alias an be
35
47
 
48
+ # Assert that the block returns truthy for the wrapped object.
49
+ #
50
+ # @parameter description [String] Failure message.
51
+ # @returns [Boolean]
36
52
  def satisfy(description="", &block)
37
53
  r = yield(@object)
38
54
  if Scampi::Counter[:depth] > 0
@@ -44,6 +60,8 @@ module Scampi
44
60
  end
45
61
  end
46
62
 
63
+ # Catch predicate calls and auto-append `?` to the method name.
64
+ # For example, `.should.be.empty` becomes `.empty?` on the object.
47
65
  def method_missing(name, *args, &block)
48
66
  name = "#{name}?" if name.to_s =~ /\w[^?]\z/
49
67
 
@@ -54,12 +72,25 @@ module Scampi
54
72
  satisfy(desc) { |x| x.__send__(name, *args, &block) }
55
73
  end
56
74
 
75
+ # Assert equality using `==`.
76
+ #
77
+ # @parameter value [Object]
57
78
  def equal(value) = self == value
79
+
80
+ # Assert the object matches a pattern using `=~`.
81
+ #
82
+ # @parameter value [Regexp]
58
83
  def match(value) = self =~ value
59
84
 
85
+ # Assert object identity (same object in memory).
86
+ #
87
+ # @parameter value [Object]
60
88
  def identical_to(value) = self.equal? value
61
89
  alias same_as identical_to
62
90
 
91
+ # Unconditionally fail the current spec.
92
+ #
93
+ # @parameter reason [String] Failure message.
63
94
  def flunk(reason="Flunked")
64
95
  raise Scampi::Error.new(:failed, reason)
65
96
  end
@@ -1,3 +1,3 @@
1
1
  module Scampi
2
- VERSION = "0.1.6"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/scampi.rb CHANGED
@@ -3,33 +3,49 @@
3
3
  # Forked from Bacon by Christian Neukirchen.
4
4
  # "Truth will sooner come out from error than from confusion." ---Francis Bacon
5
5
 
6
- # Copyright (C) 2007, 2008, 2012 Christian Neukirchen <purl.org/net/chneukirchen>
6
+ # Copyright (C) 2007, 2008, 2012 Christian Neukirchen (purl.org/net/chneukirchen)
7
7
  #
8
8
  # Scampi is freely distributable under the terms of an MIT-style license.
9
9
  # See COPYING or http://www.opensource.org/licenses/mit-license.php.
10
10
 
11
11
  require_relative 'scampi/version'
12
- require 'colorize_extended'
12
+ require_relative 'scampi/colors'
13
13
 
14
+ # The top-level Scampi module. Manages the global test queue, counters,
15
+ # error log, and TAP output.
14
16
  module Scampi
17
+ # Global counters tracking specifications, requirements, failures, errors,
18
+ # nesting depth, and whether the summary hook has been installed.
15
19
  Counter = Hash.new(0)
20
+
21
+ # Mutable string that accumulates error backtraces for TAP diagnostic output.
16
22
  ErrorLog = "".dup
23
+
24
+ # Registry of shared context blocks, keyed by name.
17
25
  Shared = Hash.new { |_, name|
18
26
  raise NameError, "no such context: #{name.inspect}"
19
27
  }
20
28
 
29
+ # Regex filter for spec names. Only specs matching this pattern will run.
21
30
  RestrictName = // unless defined? RestrictName
31
+
32
+ # Regex filter for context names. Only contexts matching this pattern will run.
22
33
  RestrictContext = // unless defined? RestrictContext
23
34
 
35
+ # Whether to include backtraces in TAP diagnostic output on failure.
24
36
  Backtraces = true unless defined? Backtraces
25
37
 
26
38
  @queue = []
27
39
  @ran = false
28
40
 
41
+ # The global queue of test items (contexts and raw specs).
42
+ #
43
+ # @returns [Array]
29
44
  def self.queue
30
45
  @queue
31
46
  end
32
47
 
48
+ # Run all queued tests and emit TAP version 14 output.
33
49
  def self.run
34
50
  return if @ran
35
51
  @ran = true
@@ -64,6 +80,39 @@ module Scampi
64
80
  puts "# #{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors"
65
81
  end
66
82
 
83
+ # Load a test file, queuing whatever specs it defines.
84
+ #
85
+ # Two styles are supported:
86
+ #
87
+ # 1. **Co-located `__END__` tests** -- the file's real code runs (Ruby
88
+ # stops parsing at `__END__`), then the section after `__END__` is
89
+ # evaluated as spec code. Because `DATA`/`__END__` is only populated
90
+ # for the directly-run script, we read and eval the tail ourselves,
91
+ # preserving the original file and line numbers for backtraces.
92
+ #
93
+ # 2. **Plain spec files** -- files with `describe`/`it` at the top level
94
+ # and no `__END__` are simply loaded.
95
+ #
96
+ # @parameter file [String] Path to the test file.
97
+ def self.load_test_file(file)
98
+ src = File.read(file)
99
+
100
+ # Run the implementation code. Ruby ignores everything past __END__.
101
+ old_verbose, $-w = $-w, nil
102
+ load file
103
+ $-w = old_verbose
104
+
105
+ # If there's an __END__ section, eval its body as spec code.
106
+ return unless src =~ /^__END__$/
107
+ head, tail = src.split(/^__END__$\n?/, 2)
108
+ return if tail.nil? || tail.strip.empty?
109
+
110
+ lineno = head.count("\n") + 2 # first line after the __END__ marker
111
+ eval(tail, TOPLEVEL_BINDING, file, lineno)
112
+ end
113
+
114
+ # Install an `at_exit` hook that runs all queued tests and sets the
115
+ # exit code to 1 if there were any failures or errors.
67
116
  def self.summary_on_exit
68
117
  return if Counter[:installed_summary] > 0
69
118
  @timer = Time.now
@@ -79,8 +128,15 @@ module Scampi
79
128
  end
80
129
  class << self; alias summary_at_exit summary_on_exit; end
81
130
 
82
- # TAP output
83
-
131
+ # Execute a single requirement block and emit the TAP ok/not-ok line.
132
+ #
133
+ # The block should return an empty string on success, or an error
134
+ # description string on failure.
135
+ #
136
+ # @parameter description [String] Human-readable spec description.
137
+ # @parameter indent [Integer] Nesting depth for TAP subtest indentation.
138
+ # @parameter local_n [Integer] The spec number within the current context.
139
+ # @returns [Boolean] Whether the requirement passed.
84
140
  def self.handle_requirement(description, indent = 0, local_n = 1)
85
141
  ErrorLog.replace ""
86
142
  error = yield
@@ -95,6 +151,12 @@ module Scampi
95
151
  end
96
152
  end
97
153
 
154
+ # Run a single spec that lives outside any describe block.
155
+ #
156
+ # @parameter description [String] Spec description.
157
+ # @parameter block [Proc] The spec body.
158
+ # @parameter n [Integer] The spec number in the top-level plan.
159
+ # @returns [Boolean] Whether the spec passed.
98
160
  def self.run_bare_spec(description, block, n)
99
161
  handle_requirement(description, 0, n) do
100
162
  begin
@@ -141,4 +203,3 @@ require_relative 'scampi/error'
141
203
  require_relative 'scampi/context'
142
204
  require_relative 'scampi/should'
143
205
  require_relative 'scampi/monkey_patches'
144
- require_relative 'scampi/kernel_ext'
data/media/scampi.jpeg ADDED
Binary file
data/scampi.gemspec CHANGED
@@ -22,8 +22,6 @@ http://github.com/general-intelligence-systems/scampi
22
22
  s.extra_rdoc_files = ['README.md']
23
23
  s.test_files = []
24
24
 
25
- s.add_dependency 'colorize-extended'
26
-
27
25
  s.author = 'Nathan K'
28
26
  s.email = 'nathankidd@hey.com'
29
27
  s.homepage = 'http://github.com/general-intelligence-systems/scampi'
metadata CHANGED
@@ -1,28 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scampi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan K
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-01 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: colorize-extended
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
11
+ dependencies: []
26
12
  description: |
27
13
  Scampi is a small RSpec clone weighing less than 350 LoC but
28
14
  nevertheless providing all essential features. Includes a
@@ -37,6 +23,7 @@ extra_rdoc_files:
37
23
  - README.md
38
24
  files:
39
25
  - ".envrc"
26
+ - CHANGELOG.md
40
27
  - COPYING
41
28
  - Gemfile
42
29
  - Gemfile.lock
@@ -50,13 +37,14 @@ files:
50
37
  - flake.nix
51
38
  - lib/rubygems_plugin.rb
52
39
  - lib/scampi.rb
40
+ - lib/scampi/colors.rb
53
41
  - lib/scampi/context.rb
54
42
  - lib/scampi/error.rb
55
- - lib/scampi/kernel_ext.rb
56
43
  - lib/scampi/monkey_patches.rb
57
44
  - lib/scampi/should.rb
58
45
  - lib/scampi/version.rb
59
46
  - lib/scampi/version.rb.erb
47
+ - media/scampi.jpeg
60
48
  - scampi.gemspec
61
49
  - test/spec_bacon.rb
62
50
  - test/spec_nontrue.rb
@@ -1,38 +0,0 @@
1
- module Kernel
2
- # Conditionally run a test block.
3
- #
4
- # When placed in a Ruby file, the block is executed only when the file is
5
- # run directly (ruby myfile.rb) or when ENV["TEST"] is set to "true".
6
- # This lets you co-locate tests alongside implementation code.
7
- #
8
- # # mylib.rb
9
- # def greet(name) = "hello #{name}"
10
- #
11
- # test do
12
- # describe "greet" do
13
- # it "says hello" do
14
- # greet("world").should == "hello world"
15
- # end
16
- # end
17
- # end
18
- #
19
- def test(&block)
20
- loc = caller_locations(1, 1).first
21
- caller_file = loc.absolute_path || loc.path
22
-
23
- if ENV["TEST"] == "true"
24
- require_relative '../scampi' unless defined?(Scampi)
25
- Scampi.summary_on_exit
26
- block.call
27
- elsif caller_file && $0
28
- program = File.expand_path($0) rescue $0
29
- caller_expanded = File.expand_path(caller_file) rescue caller_file
30
- if caller_expanded == program
31
- require_relative '../scampi' unless defined?(Scampi)
32
- Scampi.summary_on_exit
33
- block.call
34
- end
35
- end
36
- end
37
- private :test
38
- end