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 +4 -4
- data/CHANGELOG.md +30 -0
- data/Gemfile +0 -2
- data/Gemfile.lock +3 -10
- data/README.md +41 -4
- data/exe/scampi +10 -10
- data/flake.nix +5 -16
- data/lib/scampi/colors.rb +16 -0
- data/lib/scampi/context.rb +50 -5
- data/lib/scampi/error.rb +6 -0
- data/lib/scampi/monkey_patches.rb +29 -0
- data/lib/scampi/should.rb +33 -2
- data/lib/scampi/version.rb +1 -1
- data/lib/scampi.rb +66 -5
- data/media/scampi.jpeg +0 -0
- data/scampi.gemspec +0 -2
- metadata +5 -17
- data/lib/scampi/kernel_ext.rb +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1379730cbf012c3a1c4a769f7a231b15843790683b33e937152b79d02407492
|
|
4
|
+
data.tar.gz: b2e0738030762951803c4762fac51b694847d3d88e4b485c6fcfb34686ec8df0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/Gemfile.lock
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
scampi (0.1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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="$
|
|
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
|
data/lib/scampi/context.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
#
|
|
38
|
-
#
|
|
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
|
-
#
|
|
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
|
data/lib/scampi/version.rb
CHANGED
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
data/lib/scampi/kernel_ext.rb
DELETED
|
@@ -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
|