blackstart 0.2.0 → 0.6.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.
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2024 Aaron Beckerman
1
+ Copyright (c) 2024-2025 Aaron Beckerman
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the "Software"), to deal in
data/README.txt CHANGED
@@ -2,82 +2,99 @@ Blackstart
2
2
 
3
3
  Blackstart is a small, subdued library for automated testing in Ruby. It
4
4
  doesn't depend on anything beyond the Ruby platform and doesn't modify the
5
- environment outside its conventional namespace. It's tested with a primitive
6
- program, not via another automated-testing library.
5
+ environment beyond its conventional namespace. It's tested with a primitive
6
+ program, not via an automated-testing library.
7
7
 
8
- The blackstart is a small, subdued bird that eats bugs. A black start is when a
9
- power plant starts up without relying on the electrical grid.
8
+ The blackstart is a small, subdued bird that eats bugs. A black start is when
9
+ an inactive power plant restarts by means of an independent power source rather
10
+ than the electrical grid.
10
11
 
11
12
  Here's an example of a test program that uses Blackstart:
12
13
 
13
14
  require "blackstart"
14
15
 
15
- exit Blackstart.run Blackstart.add { |adder|
16
- adder.call do
16
+ exit Blackstart.run [
17
+ proc {
17
18
  unless "Hello, World!" == ["He", "", "o, Wor", "d!"].join("l")
18
19
  raise "join did not work as expected"
19
20
  end
20
- end
21
+ },
21
22
 
22
- adder.call do
23
+ proc {
23
24
  unless "sample" == "simple".gsub(/i/, "a")
24
25
  raise "gsub did not work as expected"
25
26
  end
26
- end
27
- }
28
-
29
- Blackstart.add adds procs to a collection and returns that collection. In the
30
- example, each proc is designed to run a test and, if it fails, raise an error
31
- to signal this.
27
+ }
28
+ ]
32
29
 
33
30
  Blackstart.run runs a sequence of tests and reports information about any that
34
- fail -- that is, any that raise an error. In the example, failure information
35
- would be written to the standard output stream. It returns false if there were
36
- failures; otherwise, it returns true.
31
+ fail. It returns false if there were failures, true otherwise.
32
+
33
+ An array of procs is an easy way to represent a sequence of tests. In general,
34
+ the sequence can be any object that responds to an each message in the
35
+ conventional way and each test object in the sequence can be any object that
36
+ (1) is a proc or converts to one via to_proc and (2) converts to a string via
37
+ to_s.
38
+
39
+ Each test is run by converting the test object to a proc if necessary and then
40
+ calling it in the context of a new instance of Blackstart::Scratchpad; if and
41
+ only if this raises an error (any exception that's a kind of StandardError),
42
+ Blackstart.run interprets it as a failed test.
43
+
44
+ After each test failure, Blackstart.run sends a puts message with failure
45
+ information to its second parameter, which is $stdout by default. The failure
46
+ information includes a description of the test (the test object converted to a
47
+ string) and a description of the error raised (its class, message, and
48
+ backtrace).
37
49
 
38
- The library provides little beyond this, but it's easy to do relatively
39
- sophisticated things with it. Some examples follow.
50
+ The library provides little beyond this, but it's easy to do sophisticated
51
+ things with it. Some examples follow.
40
52
 
41
53
 
42
54
  - Exiting with the appropriate status
43
55
 
44
- Blackstart.run returns false if there were any failures; otherwise, it returns
45
- true. If you use this as the argument to Kernel#exit, your test program will
46
- exit with a successful status only if there were no failures.
56
+ You may want your test program to exit with a successful status only if there
57
+ were no failures, perhaps so it can work as part of a testing script. You can
58
+ do this by using the object returned by Blackstart.run as the argument to
59
+ Kernel#exit. Blackstart.run returns false if there were failures, true
60
+ otherwise.
47
61
 
48
62
 
49
63
  - Defining helpers
50
64
 
51
- You may want all your tests to be able to assert something, generate test data,
65
+ You may want to enable all your tests to assert something, generate test data,
52
66
  or perform some other task by sending a message. You could implement this
53
67
  statelessly in a singleton object or all instances of Object, for example, but
54
- there's another option that may be more convenient. Blackstart.run calls each
55
- test proc in the context of a new Blackstart::Context instance. You can define
56
- instance methods in that class like this:
68
+ there's another option that may be more convenient. When Blackstart.run calls a
69
+ test proc, it sets the self object to a new instance of Blackstart::Scratchpad.
70
+ You can define instance methods in that class. For example:
57
71
 
58
- class Blackstart::Context
72
+ class Blackstart::Scratchpad
59
73
  def assert boolean
60
74
  raise "assertion failed" unless boolean
61
75
  end
62
76
 
63
77
  def make_products
64
78
  @cabbage = { :description => "head of cabbage", :price => 125 }
65
- @orange = { :description => "Cara cara navel orange", :price => 100 }
79
+ @orange = { :description => "Cara Cara navel orange", :price => 100 }
66
80
  nil
67
81
  end
68
82
  end
69
83
 
70
- All of your tests will be able to use them by sending messages to self. These
71
- methods can see and modify the test's state, which is disposable: it will be
72
- discarded after the test is complete and so will not affect later tests.
84
+ All your test procs can use them by sending messages to self. These methods,
85
+ like the test proc itself, can see and modify instance variables and other
86
+ elements of the scratchpad's state. The scratchpad is disposable:
87
+ Blackstart.run discards it after the test is complete.
73
88
 
74
89
 
75
90
  - Running code before and after each test
76
91
 
77
- You can run code before and after each test by defining a custom
78
- Blackstart::Context#instance_exec. For example:
92
+ You may want to run code before and after each test -- for example, to create
93
+ objects needed in tests or clean up resources without requiring every test to
94
+ do these explicitly. You can do this by defining a custom
95
+ Blackstart::Scratchpad#instance_exec. For example:
79
96
 
80
- class Blackstart::Context
97
+ class Blackstart::Scratchpad
81
98
  def instance_exec(*)
82
99
  puts "before test"
83
100
  @variable = "example"
@@ -87,66 +104,73 @@ Blackstart::Context#instance_exec. For example:
87
104
  end
88
105
  end
89
106
 
107
+ Use this hack with care because the test proc could send instance_exec messages
108
+ to self.
109
+
90
110
 
91
111
  - Building a test collection in stages
92
112
 
93
- You can build your collection of test objects in stages and run it at the end.
94
- For example:
113
+ You may want to build a test collection in stages rather than all at once. This
114
+ can be done straightforwardly:
95
115
 
96
116
  require "blackstart"
97
117
 
98
- TEST_OBJECTS = []
118
+ TESTS = []
99
119
 
100
- Blackstart.add TEST_OBJECTS do |adder|
101
- adder.call do
120
+ TESTS.concat [
121
+ proc {
102
122
  unless "Hello, World!" == ["He", "", "o, Wor", "d!"].join("l")
103
123
  raise "join did not work as expected"
104
124
  end
105
- end
106
- end
125
+ }
126
+ ]
107
127
 
108
- Blackstart.add TEST_OBJECTS do |adder|
109
- adder.call do
128
+ TESTS.concat [
129
+ proc {
110
130
  unless "sample" == "simple".gsub(/i/, "a")
111
131
  raise "gsub did not work as expected"
112
132
  end
113
- end
114
- end
133
+ }
134
+ ]
115
135
 
116
- exit Blackstart.run TEST_OBJECTS
136
+ exit Blackstart.run TESTS
117
137
 
118
- This makes it straightforward to define your tests in multiple files that you
119
- load in your test program.
138
+ This pattern is useful if you want to define your tests in multiple files: you
139
+ create a collection with an agreed-upon name, load multiple files, each of
140
+ which adds test objects to that collection, and then run all the tests.
120
141
 
121
142
 
122
143
  - Running tests in random order
123
144
 
145
+ To check if you have any tests that depend on other tests having run, or not
146
+ having run, earlier, you may want to run your tests in random order.
124
147
  Blackstart.run runs a sequence of tests in order, but you can pass it a
125
- randomly-ordered sequence. Array#shuffle may be helpful for this. If you do
126
- this, you may also want to print the random seed at the start of your program
127
- so you can re-run your tests in the same order.
148
+ randomly-ordered sequence. Array#shuffle may be helpful for this. If you use
149
+ Array#shuffle or something similar, you may also want to print the random seed
150
+ just before the tests are shuffled so you can rerun your tests in the same
151
+ order by setting the random seed.
128
152
 
129
153
 
130
154
  - Reporting detailed test descriptions
131
155
 
132
- After a test fails, Blackstart.run writes a description of the test to the
133
- output stream. It gets this description by converting the test object to a
134
- string. When the test object is an instance of Proc, which is what
135
- Blackstart.add normally produces, this string typically includes the file path
136
- and line number where it was defined: helpful, but it won't be immediately
137
- clear what was being tested.
156
+ After a test fails, Blackstart.run writes a string describing the test to the
157
+ output stream. It gets this string by sending a to_s message to the test
158
+ object. When the test object is an instance of Proc, the returned string
159
+ typically includes the file path and line number where it was defined: helpful,
160
+ but it won't be immediately clear what was being tested.
138
161
 
139
- You can improve this by making your own test objects instead of using
140
- Blackstart.add. Blackstart.run does not strictly need a sequence of Proc
141
- instances; it only needs a sequence of objects that convert to Proc instances
142
- in response to to_proc messages. Your custom test objects can respond to to_s
143
- with detailed descriptions instead of mere file paths and line numbers.
162
+ You can improve this by designing your own test objects. Blackstart.run does
163
+ not strictly need a sequence of procs; it also works with a sequence of objects
164
+ that convert to procs in response to to_proc messages. Your custom test objects
165
+ can respond to to_s with detailed descriptions instead of mere file paths and
166
+ line numbers.
144
167
 
145
168
 
146
169
  - Handling failures differently
147
170
 
148
171
  Blackstart.run handles each failure by sending a puts message to the output
149
- stream, one of its parameters. By default, this is $stdout, so the default
150
- behavior is to print unadorned failure information to standard output. But you
151
- can specify any object as the output stream -- even if it's not really a stream
152
- -- allowing you to handle failure information however you want.
172
+ stream, one of its parameters, with failure information. By default, the output
173
+ stream is $stdout, so the default behavior is to print unadorned failure
174
+ information to standard output. But you can specify any object as the output
175
+ stream -- even if it's not really a stream -- allowing you to handle failure
176
+ information however you want.
data/lib/blackstart.rb CHANGED
@@ -1,87 +1,70 @@
1
1
  ##
2
- # This module provides facilities for defining and running automated tests.
2
+ # This module provides facilities for running automated tests.
3
3
 
4
4
  module Blackstart
5
5
 
6
6
  ##
7
- # Calls the block and returns a string describing the error raised, if any.
7
+ # Yields to the block and returns a string describing the error raised, if
8
+ # any.
8
9
  #
9
- # In detail: This method sends call with no arguments to the object
10
- # representing the block. If that raises an error -- that is, an exception
11
- # whose class is StandardError or a descendent of StandardError -- this
12
- # method creates a string describing that error and returns it; the
13
- # description includes the error's class, message, and backtrace (if any). If
14
- # sending the message raises any other exception, this method raises that
15
- # exception. If sending the message does not raise an exception, this method
16
- # returns nil.
10
+ # In detail: This method yields to the block with no arguments. If that
11
+ # raises an error -- meaning an exception whose class is StandardError or a
12
+ # descendant of StandardError -- this method attempts to make and return a
13
+ # string describing that error; the description consists of the error's
14
+ # class, message, and backtrace (if any). If an exception is raised while
15
+ # making an error description, this method raises that exception. If
16
+ # yielding to the block raises a non-error exception, this method raises that
17
+ # exception. If yielding to the block does not raise an exception, this
18
+ # method returns nil.
17
19
 
18
- def self.vet &prc
19
- prc.call
20
+ def self.vet
21
+ yield
20
22
  nil
21
- rescue ::StandardError
23
+ rescue ::StandardError => e
22
24
  # Like IO#puts, separate lines with a line feed character rather than $\.
23
- ["#{$!.class}: #{$!.message}"].concat($!.backtrace || []).join "\n"
25
+ ["#{e.class}: #{e.message}"].concat(e.backtrace || []).join "\n"
24
26
  end
25
27
 
26
28
  ##
27
- # Allows the block to add procs to a collection, which it returns.
28
- #
29
- # In detail: This method sends call with one positional argument, an adder
30
- # object, to the object representing the block, director. This block can send
31
- # call messages to the adder to add objects representing the respective
32
- # blocks to the collection, sink. With each sending of call, the object to be
33
- # added will be either a proc (if call is sent with a block) or nil (if call
34
- # is sent without a block). If any non-block arguments are sent with the call
35
- # message, an exception is raised. To add the object, the adder sends a <<
36
- # message to sink with the object as the positional argument and no other
37
- # arguments. The adder returns nil in response to a call message if it
38
- # returns at all. This method returns sink.
39
- #
40
- # By default, sink is a new empty array.
41
-
42
- def self.add sink = [], &director
43
- director.call ::Kernel.lambda { |&prc|
44
- sink << prc
45
- nil
46
- }
47
- sink
48
- end
49
-
50
- ##
51
- # A class for the contexts (the self objects) of tests.
29
+ # A class for tests' disposable helper objects.
52
30
 
53
- class Context
31
+ class Scratchpad
54
32
  end
55
33
 
56
34
  ##
57
- # Runs the tests, writes any failure information to the output stream, and
35
+ # Runs tests, writes any failure information to the output stream, and
58
36
  # returns a boolean indicating whether there were no failures.
59
37
  #
60
- # In detail: This method expects the sequence of test objects, source, to
61
- # respond to an each message by yielding successive test objects to its
62
- # block. Each test is run by converting the test object to a proc and calling
63
- # it in the context of a new instance of Blackstart::Context. A failure is
64
- # when the test raises an error (that is, an exception whose class is
65
- # StandardError or a descendent of StandardError). After a failure, this
66
- # method sends a puts message to the output stream, ostream, with these
67
- # arguments (positional, in order): a string "FAILED TEST:", the test object
68
- # converted to a string via to_s, a string "...ERROR:", a string describing
69
- # the error, and an empty string. If a test raises any other exception, this
70
- # method immediately raises that exception. After it has run all the tests
71
- # and handled any failures, this method returns false if there were failures
72
- # or true otherwise.
38
+ # In detail: This method sends an each message with a block to the sequence
39
+ # of test objects, source, expecting it to yield to the block once for each
40
+ # test object in the sequence with that test object as the first argument.
41
+ # Each test involves creating a new scratchpad -- an instance of
42
+ # Blackstart::Scratchpad -- and sending an instance_exec message to it with
43
+ # the test object as a block argument, which converts the test object to a
44
+ # proc via to_proc if it's not already a proc. The resulting proc is called
45
+ # with no arguments; the self object is set to the scratchpad. A failure is
46
+ # when the test (which includes any initial conversion to a proc) raises an
47
+ # error: an exception whose class is StandardError or a descendant of
48
+ # StandardError. After a failure, this method sends a puts message to the
49
+ # output stream, ostream, with these arguments (positional, in order): a
50
+ # string "FAILED TEST:"; the test object converted to a string via to_s; a
51
+ # string "...ERROR:"; a string combining the class, message, and backtrace of
52
+ # the error; and an empty string. If a test raises a non-error exception,
53
+ # this method immediately raises that exception. After it has run all the
54
+ # tests and handled any failures, this method returns false if there were
55
+ # failures, true otherwise. This method immediately raises any exception
56
+ # raised outside of a test.
73
57
  #
74
58
  # By default, ostream is $stdout.
75
59
 
76
60
  def self.run source, ostream = $stdout
77
61
  success = true
78
- for tst in source
79
- # Convert to a proc and create a context outside the vet block so that
80
- # this method will raise any exception that results instead of
81
- # potentially treating it as a test failure.
82
- prc = ::Proc.new(&tst)
83
- context = Context.new
84
- if err_desc = vet { context.instance_exec(&prc) }
62
+ source.each do |tst|
63
+ # Create the scratchpad outside the vet block so that this method will
64
+ # raise any exception that results instead of potentially treating it as
65
+ # a test failure.
66
+ scratchpad = Scratchpad.new
67
+ if err_desc = vet { scratchpad.instance_exec(&tst) }
85
68
  success = false
86
69
  ostream.puts "FAILED TEST:", tst.to_s, "...ERROR:", err_desc.to_s, ""
87
70
  end
@@ -6,9 +6,8 @@ require "blackstart"
6
6
  # Blackstart should be a module.
7
7
  fail "" unless Module.equal? Blackstart.class
8
8
 
9
- # Blackstart should say it responds to vet, add, and run.
9
+ # Blackstart should say it responds to vet and run.
10
10
  fail "" unless Blackstart.respond_to? :vet
11
- fail "" unless Blackstart.respond_to? :add
12
11
  fail "" unless Blackstart.respond_to? :run
13
12
 
14
13
  # Test Blackstart.vet:
@@ -27,10 +26,14 @@ else
27
26
  fail ""
28
27
  end
29
28
 
30
- # If sending call with no arguments to the block raises an error,
31
- # Blackstart.vet should return a new descriptive string.
29
+ # If yielding to the block raises an error, Blackstart.vet should return a
30
+ # string describing that error.
32
31
  class ::Object
33
- fail "" unless /\ANoMethodError: / =~ Blackstart.vet
32
+ # Should return an error description when no block is given.
33
+ fail "" unless String.equal? Blackstart.vet.class
34
+
35
+ # Use frozen strings and backtrace arrays so that an exception will be raised
36
+ # if they're modified.
34
37
 
35
38
  e_class = Class.new StandardError
36
39
  def e_class.to_s
@@ -41,158 +44,73 @@ class ::Object
41
44
  def e_nil.backtrace
42
45
  nil
43
46
  end
44
- fail "" unless "FakeError: a" == Blackstart.vet { ::Kernel.raise e_nil }
47
+ error = Blackstart.vet { ::Kernel.raise e_nil }
48
+ fail "" unless String.equal? error.class
49
+ fail "" unless "FakeError: a" == error
45
50
 
46
51
  e_empty = e_class.new "b".freeze
47
52
  def e_empty.backtrace
48
53
  [].freeze
49
54
  end
50
- fail "" unless "FakeError: b" == Blackstart.vet { ::Kernel.raise e_empty }
55
+ error = Blackstart.vet { ::Kernel.raise e_empty }
56
+ fail "" unless String.equal? error.class
57
+ fail "" unless "FakeError: b" == error
51
58
 
52
59
  e_full = e_class.new "c".freeze
53
60
  def e_full.backtrace
54
61
  ["d".freeze, "e".freeze].freeze
55
62
  end
56
- fail "" unless "FakeError: c\nd\ne" ==
57
- Blackstart.vet { ::Kernel.raise e_full }
58
- end
59
-
60
- # If sending call with no arguments to the block raises a non-StandardError
61
- # exception, Blackstart.vet should not rescue it.
62
- class ::Object
63
- non_standard_error = Class.new Exception
64
- begin
65
- Blackstart.vet { ::Kernel.raise non_standard_error }
66
- rescue non_standard_error
67
- else
68
- fail ""
69
- end
70
- end
71
-
72
- # If sending call with no arguments to the block does not raise an exception,
73
- # Blackstart.vet should return nil.
74
- fail "" unless nil.equal? Blackstart.vet { nil }
75
- fail "" unless nil.equal? Blackstart.vet { true }
76
-
77
- # Test Blackstart.add:
78
-
79
- # When there is no block, Blackstart.add should raise an exception.
80
- begin
81
- Blackstart.add
82
- rescue NoMethodError
83
- else
84
- fail ""
85
- end
86
- begin
87
- Blackstart.add []
88
- rescue NoMethodError
89
- else
90
- fail ""
91
- end
92
-
93
- # Blackstart.add should raise an exception if superfluous arguments are sent.
94
- begin
95
- Blackstart.add [], "invalid"
96
- rescue ArgumentError
97
- else
98
- fail ""
99
- end
100
- begin
101
- Blackstart.add([], "invalid") {}
102
- rescue ArgumentError
103
- else
104
- fail ""
105
- end
106
-
107
- # When no collection is specified and the block does nothing, Blackstart.add
108
- # should return an empty array.
109
- fail "" unless [] == Blackstart.add {}
110
-
111
- # When a collection is specified and the block tries to add to the collection,
112
- # Blackstart.add should add corresponding procs or nil to the collection by
113
- # sending <<.
114
- class ::Object
115
- sink = [42]
116
- prc1 = proc {}
117
- prc2 = proc {}
118
- retval = Blackstart.add sink do |adder|
119
- adder.call(&prc1)
120
- adder.call(&prc2)
121
- adder.call
122
- end
123
- fail "" unless sink.equal? retval
124
- fail "" unless [42, prc1, prc2, nil] == sink
63
+ error = Blackstart.vet { ::Kernel.raise e_full }
64
+ fail "" unless String.equal? error.class
65
+ fail "" unless "FakeError: c\nd\ne" == error
125
66
  end
126
67
 
127
- # When the block sends call to the adder with arguments other than a block, it
128
- # should raise an exception and not add anything to the collection.
68
+ # If yielding to the block raises an error and that error raises an exception
69
+ # when queried, Blackstart.vet should raise that second exception.
129
70
  class ::Object
130
- sink = []
131
- begin
132
- Blackstart.add(sink) { |adder| adder.call("invalid") {} }
133
- rescue ArgumentError
134
- else
135
- fail ""
71
+ e1 = StandardError.new
72
+ e2_class = Class.new StandardError
73
+ e1.instance_variable_set :@next_exception_class, e2_class
74
+ def e1.message
75
+ ::Kernel.raise @next_exception_class
136
76
  end
137
77
  begin
138
- Blackstart.add(sink) { |adder| adder.call "invalid" }
139
- rescue ArgumentError
78
+ Blackstart.vet { ::Kernel.raise e1 }
79
+ rescue e2_class
140
80
  else
141
81
  fail ""
142
82
  end
143
- fail "" unless 0 == sink.length
144
- end
145
-
146
- # When Blackstart.add's block sends call to the adder and it returns, nil
147
- # should be the object it returns.
148
- class ::Object
149
- retval_block = retval_no_block = true
150
- Blackstart.add do |adder|
151
- retval_block = adder.call {}
152
- retval_no_block = adder.call
153
- end
154
- fail "" unless nil.equal? retval_block
155
- fail "" unless nil.equal? retval_no_block
156
83
  end
157
84
 
158
- # In general, when there is a block that raises an exception, Blackstart.add
159
- # should not rescue it.
85
+ # If yielding to the block raises a non-StandardError exception, Blackstart.vet
86
+ # should raise it.
160
87
  class ::Object
161
- e = StandardError.new
88
+ non_standard_error = Class.new Exception
162
89
  begin
163
- Blackstart.add { ::Kernel.raise e }
164
- rescue
165
- fail "" unless e.equal? $!
90
+ Blackstart.vet { ::Kernel.raise non_standard_error }
91
+ rescue non_standard_error
166
92
  else
167
93
  fail ""
168
94
  end
169
95
  end
170
96
 
171
- # When there is a block that tries to add to the collection and the collection
172
- # responds to the << message by raising an exception, Blackstart.add should not
173
- # rescue it.
174
- begin
175
- Blackstart.add(nil) { |adder| adder.call {} }
176
- rescue NoMethodError
177
- else
178
- fail ""
179
- end
97
+ # If yielding to the block does not raise an exception, Blackstart.vet should
98
+ # return nil. The object returned shouldn't matter.
99
+ fail "" unless nil.equal? Blackstart.vet { nil }
100
+ fail "" unless nil.equal? Blackstart.vet { true }
180
101
 
181
- # When no collection is specified and the block tries to add to the collection,
182
- # Blackstart.add should append to a new array that gets returned.
183
- class ::Object
184
- prc = proc {}
185
- fail "" unless [prc] == Blackstart.add { |adder| adder.call(&prc) }
186
- end
102
+ # Test Blackstart::Scratchpad:
187
103
 
188
- # Test Blackstart::Context:
104
+ # Blackstart::Scratchpad.new should return an instance of
105
+ # Blackstart::Scratchpad, which implies that Blackstart::Scratchpad should be a
106
+ # class.
107
+ fail "" unless Blackstart::Scratchpad.equal? Blackstart::Scratchpad.new.class
189
108
 
190
- # Blackstart::Context.new should return an instance of Blackstart::Context,
191
- # which implies that Blackstart::Context should be a class.
192
- fail "" unless Blackstart::Context.equal? Blackstart::Context.new.class
109
+ # Blackstart::Scratchpad should not be frozen.
110
+ fail "" if Blackstart::Scratchpad.frozen?
193
111
 
194
- # A new context should have no instance variables.
195
- fail "" unless 0 == Blackstart::Context.new.instance_variables.length
112
+ # A new scratchpad should have no instance variables.
113
+ fail "" unless 0 == Blackstart::Scratchpad.new.instance_variables.length
196
114
 
197
115
  # Test Blackstart.run:
198
116
 
@@ -200,7 +118,7 @@ fail "" unless 0 == Blackstart::Context.new.instance_variables.length
200
118
  # return the appropriate object based on the behavior of the tests.
201
119
  class ::Object
202
120
  begin
203
- # Set $stdout to a test spy and set it back in the ensure clause.
121
+ # Set $stdout to a test spy and restore it in the ensure clause.
204
122
  original_stdout = $stdout
205
123
  spy_stdout = $stdout.clone
206
124
  spy_stdout.instance_variable_set :@_test_calls, stdout_calls = []
@@ -210,155 +128,235 @@ class ::Object
210
128
  end
211
129
  $stdout = spy_stdout
212
130
 
213
- # No tests.
214
- source = [].freeze
215
- fail "" unless true.equal? Blackstart.run(source, Object.new)
131
+ invalid_ostream = Object.new
132
+ def invalid_ostream.puts(*)
133
+ ::Kernel.raise ::NoMethodError
134
+ end
135
+
136
+ # When there are no tests, Blackstart.run should not notify the output
137
+ # stream of any failures.
138
+ source = []
139
+ fail "" unless true.equal? Blackstart.run(source, invalid_ostream)
216
140
  fail "" unless true.equal? Blackstart.run(source, spy_stdout)
217
141
  fail "" unless true.equal? Blackstart.run(source)
218
142
  fail "" unless 0 == stdout_calls.length
219
143
 
220
- # Tests that do not raise exceptions.
221
- source = [proc {}, proc { 42 }].freeze
222
- fail "" unless true.equal? Blackstart.run(source, Object.new)
144
+ # When there are tests but none raise exceptions, Blackstart.run should not
145
+ # notify the output stream of any failures. The objects returned by the
146
+ # test procs shouldn't affect Blackstart.run's behavior.
147
+ source = [proc {}, proc { 42 }]
148
+ fail "" unless true.equal? Blackstart.run(source, invalid_ostream)
223
149
  fail "" unless true.equal? Blackstart.run(source, spy_stdout)
224
150
  fail "" unless true.equal? Blackstart.run(source)
225
151
  fail "" unless 0 == stdout_calls.length
226
152
 
227
- # Define some example test objects.
228
- e_class = Class.new StandardError
153
+ # Define some test objects. Freeze them, the strings to which they convert,
154
+ # some exception details, and the strings to which the exception classes
155
+ # convert to check that they don't get modified.
156
+ e_class = Class.new StandardError do
157
+ def backtrace
158
+ ["line1".freeze, "line2".freeze].freeze
159
+ end
160
+ end
229
161
  def e_class.to_s
230
- "FakeError"
162
+ "FakeError".freeze
231
163
  end
232
- fail_prc1 = proc { ::Kernel.raise e_class, "fake message 1" }
164
+ fail_prc1 = proc { ::Kernel.raise e_class, "fake message 1".freeze }
233
165
  def fail_prc1.to_s
234
- "to_s 1"
166
+ "to_s 1".freeze
235
167
  end
168
+ fail_prc1.freeze
169
+ pass_prc = proc {}.freeze
236
170
  fail_prc2 = Object.new # Not a Proc instance, but converts to one.
237
171
  fail_prc2.instance_variable_set :@_test_e_class, e_class
238
172
  def fail_prc2.to_proc
239
173
  e_class = @_test_e_class
240
- ::Proc.new { ::Kernel.raise e_class, "fake message 2" }
174
+ ::Proc.new { ::Kernel.raise e_class, "fake message 2".freeze }.freeze
241
175
  end
242
176
  def fail_prc2.to_s
243
- "to_s 2"
177
+ "to_s 2".freeze
244
178
  end
245
- pass_prc = proc {}
246
-
247
- # A mix of tests that raise errors and tests that return.
248
- source = [fail_prc1, fail_prc2, pass_prc].freeze
249
-
250
- # Output stream that does not conform to the expected interface. (And in
251
- # general, the only exceptions Blackstart.run should rescue are
252
- # StandardError exceptions raised by tests.)
253
- begin
254
- Blackstart.run source, Object.new
255
- rescue NoMethodError
256
- else
257
- fail ""
179
+ fail_prc2.freeze
180
+ non_prc = Object.new # Does not convert to a proc.
181
+ def non_prc.to_proc
182
+ ::Kernel.raise ::NoMethodError
258
183
  end
259
- fail "" unless 0 == stdout_calls.length
184
+ def non_prc.to_s
185
+ "to_s 3".freeze
186
+ end
187
+ non_prc.freeze
188
+ bad_prc = Object.new # Pretends to convert to a proc but doesn't.
189
+ def bad_prc.to_proc
190
+ 0
191
+ end
192
+ def bad_prc.to_s
193
+ "to_s 4".freeze
194
+ end
195
+ bad_prc.freeze
196
+
197
+ # A mix of tests that raise errors and tests that return. Freeze the array
198
+ # to check that it's not being modified.
199
+ source = [fail_prc1, pass_prc, fail_prc2, non_prc, bad_prc].freeze
260
200
 
261
- # Output stream that conforms to the expected interface.
201
+ # An output stream that conforms to the expected interface and does not
202
+ # raise an exception should receive all failure information.
262
203
  spy_ostream = Object.new
263
204
  spy_ostream.instance_variable_set :@_test_calls, calls = []
264
205
  def spy_ostream.puts *args
265
206
  @_test_calls << args
266
- nil
207
+ # A real standard output stream would return nil here (and spy_stdout
208
+ # does that), but Blackstart.run's behavior should not be affected by the
209
+ # returned object. Returning this unusual object tests that.
210
+ ::Object.new
267
211
  end
212
+ # Freeze the output stream to check that it's not being modified.
213
+ spy_ostream.freeze
268
214
  fail "" unless false.equal? Blackstart.run(source, spy_ostream)
269
215
  fail "" unless 0 == stdout_calls.length
270
- fail "" unless 2 == calls.length
216
+ fail "" unless 4 == calls.length
271
217
  fail "" unless 5 == calls[0].length
218
+ fail "" unless calls[0].all? { |o| ::String.equal? o.class }
272
219
  fail "" unless "FAILED TEST:" == calls[0][0]
273
220
  fail "" unless "to_s 1" == calls[0][1]
274
221
  fail "" unless "...ERROR:" == calls[0][2]
275
- fail "" unless /\AFakeError: fake message 1$/ =~ calls[0][3]
222
+ fail "" unless "FakeError: fake message 1\nline1\nline2" == calls[0][3]
276
223
  fail "" unless "" == calls[0][4]
277
224
  fail "" unless 5 == calls[1].length
225
+ fail "" unless calls[1].all? { |o| ::String.equal? o.class }
278
226
  fail "" unless "FAILED TEST:" == calls[1][0]
279
227
  fail "" unless "to_s 2" == calls[1][1]
280
228
  fail "" unless "...ERROR:" == calls[1][2]
281
- fail "" unless /\AFakeError: fake message 2$/ =~ calls[1][3]
229
+ fail "" unless "FakeError: fake message 2\nline1\nline2" == calls[1][3]
282
230
  fail "" unless "" == calls[1][4]
283
- spy_ostream = calls = nil
284
-
285
- # Default output stream.
231
+ fail "" unless 5 == calls[2].length
232
+ fail "" unless calls[2].all? { |o| ::String.equal? o.class }
233
+ fail "" unless "FAILED TEST:" == calls[2][0]
234
+ fail "" unless "to_s 3" == calls[2][1]
235
+ fail "" unless "...ERROR:" == calls[2][2]
236
+ # (No need to check the error description.)
237
+ fail "" unless "" == calls[2][4]
238
+ fail "" unless 5 == calls[3].length
239
+ fail "" unless calls[3].all? { |o| ::String.equal? o.class }
240
+ fail "" unless "FAILED TEST:" == calls[3][0]
241
+ fail "" unless "to_s 4" == calls[3][1]
242
+ fail "" unless "...ERROR:" == calls[3][2]
243
+ # (No need to check the error description.)
244
+ fail "" unless "" == calls[3][4]
245
+
246
+ # The faked standard output stream.
286
247
  stdout_calls.clear
287
248
  fail "" unless false.equal? Blackstart.run(source)
288
- fail "" unless 2 == stdout_calls.length
249
+ fail "" unless 4 == stdout_calls.length
289
250
  fail "" unless 5 == stdout_calls[0].length
251
+ fail "" unless stdout_calls[0].all? { |o| ::String.equal? o.class }
290
252
  fail "" unless "FAILED TEST:" == stdout_calls[0][0]
291
253
  fail "" unless "to_s 1" == stdout_calls[0][1]
292
254
  fail "" unless "...ERROR:" == stdout_calls[0][2]
293
- fail "" unless /\AFakeError: fake message 1$/ =~ stdout_calls[0][3]
255
+ fail "" unless "FakeError: fake message 1\nline1\nline2" ==
256
+ stdout_calls[0][3]
294
257
  fail "" unless "" == stdout_calls[0][4]
295
258
  fail "" unless 5 == stdout_calls[1].length
259
+ fail "" unless stdout_calls[1].all? { |o| ::String.equal? o.class }
296
260
  fail "" unless "FAILED TEST:" == stdout_calls[1][0]
297
261
  fail "" unless "to_s 2" == stdout_calls[1][1]
298
262
  fail "" unless "...ERROR:" == stdout_calls[1][2]
299
- fail "" unless /\AFakeError: fake message 2$/ =~ stdout_calls[1][3]
263
+ fail "" unless "FakeError: fake message 2\nline1\nline2" ==
264
+ stdout_calls[1][3]
300
265
  fail "" unless "" == stdout_calls[1][4]
266
+ fail "" unless 5 == stdout_calls[2].length
267
+ fail "" unless stdout_calls[2].all? { |o| ::String.equal? o.class }
268
+ fail "" unless "FAILED TEST:" == stdout_calls[2][0]
269
+ fail "" unless "to_s 3" == stdout_calls[2][1]
270
+ fail "" unless "...ERROR:" == stdout_calls[2][2]
271
+ # (No need to check the error description.)
272
+ fail "" unless "" == stdout_calls[2][4]
273
+ fail "" unless 5 == stdout_calls[3].length
274
+ fail "" unless stdout_calls[3].all? { |o| ::String.equal? o.class }
275
+ fail "" unless "FAILED TEST:" == stdout_calls[3][0]
276
+ fail "" unless "to_s 4" == stdout_calls[3][1]
277
+ fail "" unless "...ERROR:" == stdout_calls[3][2]
278
+ # (No need to check the error description.)
279
+ fail "" unless "" == stdout_calls[3][4]
301
280
  stdout_calls.clear
302
281
 
303
- # Test that raises a non-StandardError exception. (And in general, the only
304
- # exceptions Blackstart.run should rescue are StandardError exceptions
305
- # raised by tests.)
306
- non_standard_error = Class.new Exception
307
- source = [proc { ::Kernel.raise non_standard_error }]
282
+ # An output stream that raises an exception when notified of a failure
283
+ # should cause Blackstart.run to raise that exception and stop processing
284
+ # tests. (In general, the only exceptions Blackstart.run should rescue are
285
+ # StandardError exceptions raised by tests.)
286
+ second_test_run = false
287
+ source = [proc { ::Kernel.raise "" }, proc { second_test_run = true }]
308
288
  begin
309
- Blackstart.run source, spy_stdout
310
- rescue non_standard_error
289
+ Blackstart.run source, invalid_ostream
290
+ rescue NoMethodError
311
291
  else
312
292
  fail ""
313
293
  end
294
+ fail "" if second_test_run
314
295
  fail "" unless 0 == stdout_calls.length
315
296
 
316
- # Test sequence that does not conform to the interface. (And in general,
317
- # the only exceptions Blackstart.run should rescue are StandardError
318
- # exceptions raised by tests.)
297
+ # When a test raises a non-StandardError exception, Blackstart.run should
298
+ # raise that exception and stop processing tests. (In general, the only
299
+ # exceptions Blackstart.run should rescue are StandardError exceptions
300
+ # raised by tests.)
301
+ second_test_run = false
302
+ non_standard_error = Class.new Exception
303
+ source = [proc { ::Kernel.raise non_standard_error },
304
+ proc { second_test_run = true }]
319
305
  begin
320
- Blackstart.run nil, spy_stdout
321
- rescue NoMethodError
306
+ Blackstart.run source, spy_stdout
307
+ rescue non_standard_error
322
308
  else
323
309
  fail ""
324
310
  end
311
+ fail "" if second_test_run
325
312
  fail "" unless 0 == stdout_calls.length
326
313
 
327
- # Test sequences containing objects that do not convert to procs (and do
328
- # not pretend to) and objects that pretend to convert to procs but do not.
329
- # (And in general, the only exceptions Blackstart.run should rescue are
314
+ # When the test sequence does not conform to the interface and raises an
315
+ # exception during iteration, Blackstart.run should raise that exception.
316
+ # (In general, the only exceptions Blackstart.run should rescue are
330
317
  # StandardError exceptions raised by tests.)
331
- source = [nil]
332
- begin
333
- Blackstart.run source, spy_stdout
334
- rescue ArgumentError
335
- else
336
- fail ""
318
+ source = Object.new
319
+ def source.each(*)
320
+ ::Kernel.raise ::NoMethodError
337
321
  end
338
- source = [0]
339
322
  begin
340
323
  Blackstart.run source, spy_stdout
341
- rescue TypeError
324
+ rescue NoMethodError
342
325
  else
343
326
  fail ""
344
327
  end
345
- bad_to_proc = Object.new
346
- def bad_to_proc.to_proc
347
- nil
328
+ fail "" unless 0 == stdout_calls.length
329
+
330
+ # When a test fails and converting the test object to a string raises an
331
+ # exception, Blackstart.run should raise that exception and stop processing
332
+ # tests. (In general, the only exceptions Blackstart.run should rescue are
333
+ # StandardError exceptions raised by tests.)
334
+ second_test_run = false
335
+ no_desc = proc { ::Kernel.raise "" }
336
+ def no_desc.to_s
337
+ ::Kernel.raise ::NoMethodError
348
338
  end
349
- source = [bad_to_proc]
339
+ source = [no_desc, proc { second_test_run = true }]
350
340
  begin
351
341
  Blackstart.run source, spy_stdout
352
- rescue TypeError
342
+ rescue NoMethodError
353
343
  else
354
344
  fail ""
355
345
  end
346
+ fail "" if second_test_run
356
347
  fail "" unless 0 == stdout_calls.length
357
348
  ensure
358
349
  $stdout = original_stdout
359
350
  end
360
351
  end
361
352
 
353
+ # Blackstart.run should run tests in order.
354
+ class ::Object
355
+ data = []
356
+ Blackstart.run [proc { data << 1 }, proc { data << 2 }], nil
357
+ fail "" unless [1, 2] == data
358
+ end
359
+
362
360
  # Blackstart.run should call each test proc with no arguments.
363
361
  class ::Object
364
362
  args = nil
@@ -369,14 +367,55 @@ class ::Object
369
367
  fail "" unless [] == args
370
368
  end
371
369
 
372
- # Blackstart.run should call each test proc in the context of a unique instance
373
- # of Blackstart::Context.
370
+ # Blackstart.run should work with any source that responds to each. The object
371
+ # returned in response to the each message should not affect Blackstart.run's
372
+ # behavior.
374
373
  class ::Object
375
- context1 = context2 = nil
376
- Blackstart.run [proc { context1 = self }, proc { context2 = self }], nil
377
- fail "" unless Blackstart::Context.equal? context1.class
378
- fail "" unless Blackstart::Context.equal? context2.class
379
- fail "" if context1.equal? context2
374
+ source = Object.new
375
+ def source.each
376
+ ran = false
377
+ yield proc { ran = true }
378
+ @ran = ran
379
+ self
380
+ end
381
+ fail "" unless true.equal? Blackstart.run(source, nil)
382
+ fail "" unless source.instance_variable_get :@ran
383
+
384
+ source = Object.new
385
+ def source.each
386
+ yield proc {}
387
+ nil
388
+ end
389
+ fail "" unless true.equal? Blackstart.run(source, nil)
390
+ end
391
+
392
+ # Blackstart.run should send exactly one each message to the source.
393
+ class ::Object
394
+ source = Object.new
395
+ source.instance_variable_set :@count, 0
396
+ def source.each
397
+ @count += 1
398
+ self
399
+ end
400
+ Blackstart.run source, nil
401
+ fail "" unless 1 == source.instance_variable_get(:@count)
402
+ end
403
+
404
+ # When Blackstart.run calls a test proc, the self object should be a unique
405
+ # instance of Blackstart::Scratchpad.
406
+ class ::Object
407
+ sp1 = sp2 = nil
408
+ Blackstart.run [proc { sp1 = self }, proc { sp2 = self }], nil
409
+ fail "" unless Blackstart::Scratchpad.equal? sp1.class
410
+ fail "" unless Blackstart::Scratchpad.equal? sp2.class
411
+ fail "" if sp1.equal? sp2
412
+ end
413
+
414
+ # When Blackstart.run runs a test, the scratchpad should not be frozen.
415
+ class ::Object
416
+ frozen = true
417
+ Blackstart.run [proc { frozen = frozen? }], nil
418
+ fail "" if frozen
380
419
  end
381
420
 
382
421
  # Blackstart.run should raise an exception if too few or too many non-block
@@ -387,11 +426,34 @@ rescue ArgumentError
387
426
  else
388
427
  fail ""
389
428
  end
390
- begin
391
- Blackstart.run [], nil, "invalid"
392
- rescue ArgumentError
393
- else
394
- fail ""
429
+ class ::Object
430
+ source = []
431
+ begin
432
+ Blackstart.run source, nil, nil
433
+ rescue ArgumentError
434
+ else
435
+ fail ""
436
+ end
437
+ end
438
+
439
+ # The following section modifies the state of the library, so it must be run
440
+ # after all the ordinary tests.
441
+
442
+ # Blackstart.run should run each test by sending instance_exec to a scratchpad.
443
+ class Blackstart::Scratchpad
444
+ def foo
445
+ 2
446
+ end
447
+
448
+ def instance_exec(*)
449
+ @ivar = 1
450
+ super
451
+ end
452
+ end
453
+ class ::Object
454
+ result = nil
455
+ Blackstart.run [proc { result = [@ivar, foo()] }], nil
456
+ fail "" unless [1, 2] == result
395
457
  end
396
458
 
397
459
  # If this message doesn't get written, there was a problem.
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blackstart
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
8
+ - 6
9
9
  - 0
10
- version: 0.2.0
10
+ version: 0.6.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Aaron Beckerman
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2024-05-14 00:00:00 -07:00
18
+ date: 2025-05-16 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -30,7 +30,6 @@ extra_rdoc_files: []
30
30
  files:
31
31
  - LICENSE.txt
32
32
  - README.txt
33
- - blackstart.gemspec
34
33
  - lib/blackstart.rb
35
34
  - test/blackstart_test.rb
36
35
  has_rdoc: true
@@ -69,5 +68,5 @@ rubygems_version: 1.6.2
69
68
  signing_key:
70
69
  specification_version: 3
71
70
  summary: A small, subdued library for automated testing.
72
- test_files:
73
- - test/blackstart_test.rb
71
+ test_files: []
72
+
data/blackstart.gemspec DELETED
@@ -1,11 +0,0 @@
1
- Gem::Specification.new do |s|
2
- s.name = "blackstart"
3
- s.version = "0.2.0"
4
- s.authors = ["Aaron Beckerman"]
5
- s.summary = "A small, subdued library for automated testing."
6
- s.licenses = ["MIT"]
7
- s.required_ruby_version = ">= 1.8.7"
8
- s.files = ["LICENSE.txt", "README.txt", "blackstart.gemspec",
9
- "lib/blackstart.rb", "test/blackstart_test.rb"]
10
- s.test_files = ["test/blackstart_test.rb"]
11
- end