slightish 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +63 -0
- data/.travis.yml +8 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +140 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/slightish +5 -0
- data/lib/slightish/command.rb +63 -0
- data/lib/slightish/sandbox.rb +18 -0
- data/lib/slightish/test_case.rb +132 -0
- data/lib/slightish/test_suite.rb +167 -0
- data/lib/slightish/version.rb +3 -0
- data/lib/slightish.rb +10 -0
- data/lib/string_mixins.rb +78 -0
- data/slightish.gemspec +31 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 8dcf08230b1ba15f62dfe96a21b16a320603501e
|
|
4
|
+
data.tar.gz: b651c070d9f46f071709a10cce04b1208856a8a8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bca0ad40555d5ce4b79c2a657fe6ddafa2f8825f3913e202c735fd98d70ea3dba8130db90bd498d8321d06b9f4fd98e49c5fe58699a165fea4d05fc1f02765a5
|
|
7
|
+
data.tar.gz: e1a340b606a1e38ec9919a77a3843c9e6493bf34defd56fa437571850837f044ec0a8bc8f81230e08b0a9825b10ea87ef55fed94f2e5b012cd60fe47166d45aa
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
Include:
|
|
3
|
+
- 'lib/**/*'
|
|
4
|
+
- 'bin/*'
|
|
5
|
+
- 'exe/*'
|
|
6
|
+
- 'test/**/*.rb'
|
|
7
|
+
Exclude:
|
|
8
|
+
- 'bin/setup'
|
|
9
|
+
- 'vendor/**/*'
|
|
10
|
+
|
|
11
|
+
Style/FileName:
|
|
12
|
+
Exclude:
|
|
13
|
+
- 'Gemfile'
|
|
14
|
+
- 'Rakefile'
|
|
15
|
+
|
|
16
|
+
# Using %[...] to avoid conflicts with parens in shell expansions
|
|
17
|
+
Style/PercentLiteralDelimiters:
|
|
18
|
+
Exclude:
|
|
19
|
+
- 'test/**/*_test.rb'
|
|
20
|
+
- 'test/**/*_tests.rb'
|
|
21
|
+
|
|
22
|
+
# Need trailing whitespace for some test suites
|
|
23
|
+
Layout/TrailingWhitespace:
|
|
24
|
+
Exclude:
|
|
25
|
+
- 'test/**/*_test.rb'
|
|
26
|
+
- 'test/**/*_tests.rb'
|
|
27
|
+
|
|
28
|
+
Metrics/ParameterLists:
|
|
29
|
+
Exclude:
|
|
30
|
+
- 'test/lib/slightish_test.rb'
|
|
31
|
+
|
|
32
|
+
Metrics/LineLength:
|
|
33
|
+
Enabled: false
|
|
34
|
+
|
|
35
|
+
Style/Documentation:
|
|
36
|
+
Enabled: false
|
|
37
|
+
|
|
38
|
+
Style/ClassAndModuleChildren:
|
|
39
|
+
Enabled: false
|
|
40
|
+
|
|
41
|
+
Style/BracesAroundHashParameters:
|
|
42
|
+
Enabled: false
|
|
43
|
+
|
|
44
|
+
Layout/CommentIndentation:
|
|
45
|
+
Enabled: false
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Ideally these are TODOs:
|
|
49
|
+
|
|
50
|
+
Metrics/AbcSize:
|
|
51
|
+
Enabled: false
|
|
52
|
+
|
|
53
|
+
Metrics/BlockLength:
|
|
54
|
+
Enabled: false
|
|
55
|
+
|
|
56
|
+
Metrics/CyclomaticComplexity:
|
|
57
|
+
Enabled: false
|
|
58
|
+
|
|
59
|
+
Metrics/MethodLength:
|
|
60
|
+
Enabled: false
|
|
61
|
+
|
|
62
|
+
Metrics/PerceivedComplexity:
|
|
63
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at tim.clem@gmail.com. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
|
72
|
+
|
|
73
|
+
[homepage]: http://contributor-covenant.org
|
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Tim Clem
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Slightish
|
|
2
|
+
|
|
3
|
+
*Literate testing of shell scripts*
|
|
4
|
+
|
|
5
|
+
[](https://travis-ci.org/misterfifths/slightish) [](https://coveralls.io/github/misterfifths/slightish?branch=master) [](https://rubygems.org/gems/slightish)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### What's this then?
|
|
9
|
+
|
|
10
|
+
A spiritual successor to [tush](https://github.com/darius/tush), *slightish* is a simple tool for testing command line tools using a simple syntax.
|
|
11
|
+
|
|
12
|
+
### Installation
|
|
13
|
+
|
|
14
|
+
*slightish* can be install through RubyGems, the Ruby package manager. Run the following command, optionally prefixing it with `sudo` if your environment requires it (you'll get some sort of permissions error if so).
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gem install slightish
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Writing tests
|
|
21
|
+
|
|
22
|
+
Write your tests interspersed among your documentation, in whatever file type you please (Markdown, plain text, HTML, etc.). For example, *this very file* can be used as a test. Blocks of text that look like shell transcripts are executed and tested:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
$ echo 'this is a test'
|
|
26
|
+
| this is a test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can also compare stderr and exit codes:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
$ echo stdout; echo stderr >&2; echo "more stdout"; exit 2
|
|
33
|
+
| stdout
|
|
34
|
+
| more stdout
|
|
35
|
+
@ stderr
|
|
36
|
+
? 2
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That example covers 90% of the functionality. The syntax in detail works like this:
|
|
40
|
+
|
|
41
|
+
- Lines that start with `$ ` are interpreted as commands to run in the shell. The `$` must be in the first column of the file and must be followed by a space.
|
|
42
|
+
- Lines starting with `| ` specify the stdout of the most recent command. As with `$ `, the `|` must be in the first column and must be followed by a space. You may specify more than one `| ` line to test for multiline output.
|
|
43
|
+
- Lines starting with `@ ` specify the stderr of the most recent command. You may also specify more than one `@ ` line per command.
|
|
44
|
+
- A line of the form `? <positive integer>` specifies the expected exit code of the most recent command. You may omit a `? ` line for an expected exit code of zero.
|
|
45
|
+
|
|
46
|
+
Specifying any of the above magic lines out of order is a syntax error; you must specify a command (`$ `), and then optionally stdout (`| `), stderr (`@ `), and the exit code (`? `). If a command is expected to produce no output and have an exit code of zero, you may omit everything but the `$ ` line:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# This passes because it exits with code 0 and produces no output
|
|
50
|
+
$ echo
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Running tests
|
|
54
|
+
|
|
55
|
+
Once you've written tests, pass the filenames to the `slightish` command:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
slightish my-first-test.md my-second-test
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This will run all the tests in all the specified files, and output details about any failures. The command will exit with status code 1 if any tests fail.
|
|
62
|
+
|
|
63
|
+
Let's add a failing test real quick:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
$ exit 2
|
|
67
|
+
| 1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Now if we run `slightish` on this file, we get this output:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
❌ README.md:64-65
|
|
74
|
+
Expected stdout:
|
|
75
|
+
1
|
|
76
|
+
Actual stdout: empty
|
|
77
|
+
|
|
78
|
+
Expected exit code: 0
|
|
79
|
+
Actual exit code: 2
|
|
80
|
+
|
|
81
|
+
----------
|
|
82
|
+
README.md 6 passed 1 failed
|
|
83
|
+
|
|
84
|
+
Total tests: 7
|
|
85
|
+
Passed: 6
|
|
86
|
+
Failed: 1
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### More features
|
|
90
|
+
|
|
91
|
+
#### Sandboxes
|
|
92
|
+
|
|
93
|
+
Each test file is run in its own sandbox directory, so you can safely write to files if your tests require it:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
$ echo 'hello world' > test-file
|
|
97
|
+
$ cat test-file
|
|
98
|
+
| hello world
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
All sandbox directories are deleted at the end of testing.
|
|
102
|
+
|
|
103
|
+
If you have a directory of files that your tests require ("fixtures"), you can specify it as the template for sandboxes, and its contents are copied to each sandbox before tests begin. This is done by setting the environmental variable `SLIGHTISH_TEMPLATE_DIR` before invoking the `slightish` command.
|
|
104
|
+
|
|
105
|
+
Since sandboxes ensure (or at least attempt to ensure) that each test file is independent of the others, the tests in each test files are run in parallel to speed up the process. Commands within each test file are run in order, however.
|
|
106
|
+
|
|
107
|
+
#### Multiline commands
|
|
108
|
+
|
|
109
|
+
If you need to specify a long command, you can split it onto multiple lines by ending the `$ ` line with a backslash:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
$ echo "This is a \
|
|
113
|
+
very long string \
|
|
114
|
+
that I wish to print."
|
|
115
|
+
| This is a very long string that I wish to print.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Note that the backslash must be the final character on the line, or else it is not treated as a continuation.
|
|
119
|
+
|
|
120
|
+
### More examples
|
|
121
|
+
|
|
122
|
+
The [tests for jutil](https://github.com/misterfifths/jutil/tree/master/tests), a tool to manipulate JSON on the command line, were written in tush/slightish syntax.
|
|
123
|
+
|
|
124
|
+
Also see [tests for adolfopa/cstow](https://github.com/adolfopa/cstow/blob/master/src/TESTS.md).
|
|
125
|
+
|
|
126
|
+
### Acknowledgements
|
|
127
|
+
|
|
128
|
+
Thanks to [darius](https://github.com/darius) for the original [tush](https://github.com/darius/tush), the syntax of which I adopted. Thanks also to [adolfopa](https://github.com/adolfopa/) for his [fork](https://github.com/adolfopa/tush) which added support for multiline commands.
|
|
129
|
+
|
|
130
|
+
### Future plans
|
|
131
|
+
|
|
132
|
+
In no particular order,
|
|
133
|
+
|
|
134
|
+
- Regex matching for stdout and stderr
|
|
135
|
+
- Expose some things (sandbox template dir) as command line arguments
|
|
136
|
+
- More responsive and prettier output
|
|
137
|
+
- Fail fast mode
|
|
138
|
+
- Smarter threading?
|
|
139
|
+
- Bless
|
|
140
|
+
- Diff output
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'rake/testtask'
|
|
2
|
+
require 'rubocop/rake_task'
|
|
3
|
+
|
|
4
|
+
desc 'Run tests'
|
|
5
|
+
Rake::TestTask.new do |t|
|
|
6
|
+
ENV.delete('SLIGHTISH_TEMPLATE_DIR')
|
|
7
|
+
ENV['SLIGHTISH_NO_COLOR'] = '1'
|
|
8
|
+
ENV['SLIGHTISH_NO_WARNINGS'] = '1'
|
|
9
|
+
t.test_files = FileList['test/**/*_test{s,}.rb']
|
|
10
|
+
end
|
|
11
|
+
task default: :test
|
|
12
|
+
|
|
13
|
+
desc 'Run rubocop'
|
|
14
|
+
RuboCop::RakeTask.new(:rubocop)
|
|
15
|
+
|
|
16
|
+
desc 'Run rubocop'
|
|
17
|
+
task lint: :rubocop
|
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'slightish'
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require 'irb'
|
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/slightish
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'slightish/test_suite'
|
|
2
|
+
|
|
3
|
+
class Slightish::Command
|
|
4
|
+
def self.run(argv)
|
|
5
|
+
if argv.empty? || argv.include?('--help') || argv.include?('-h')
|
|
6
|
+
print_usage
|
|
7
|
+
Process.exit(2)
|
|
8
|
+
else
|
|
9
|
+
new.run(argv, sandbox_template_dir: ENV['SLIGHTISH_TEMPLATE_DIR'])
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.print_usage
|
|
14
|
+
$stderr.puts('Literate testing of shell tools')
|
|
15
|
+
$stderr.puts('usage: slightish <file...>')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(test_files, sandbox_template_dir: nil)
|
|
19
|
+
Thread.abort_on_exception = true
|
|
20
|
+
|
|
21
|
+
suites = []
|
|
22
|
+
worker_threads = []
|
|
23
|
+
|
|
24
|
+
test_files.each do |file|
|
|
25
|
+
suite = Slightish::TestSuite.from_file(file, sandbox_template_dir: sandbox_template_dir)
|
|
26
|
+
suites << suite
|
|
27
|
+
worker_threads << Thread.new { suite.run }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
worker_threads.each(&:join)
|
|
31
|
+
|
|
32
|
+
suites.each do |suite|
|
|
33
|
+
puts(suite.failure_description) if suite.failed?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts('----------') if suites.any?(&:failed?)
|
|
37
|
+
|
|
38
|
+
total_tests = 0
|
|
39
|
+
total_passed = 0
|
|
40
|
+
total_failed = 0
|
|
41
|
+
max_suite_name_length = suites.max_by { |suite| suite.name.length }.name.length
|
|
42
|
+
max_passed_length = suites.max_by(&:passed_count).passed_count.to_s.length
|
|
43
|
+
max_failed_length = suites.max_by(&:failed_count).failed_count.to_s.length
|
|
44
|
+
|
|
45
|
+
suites.each do |suite|
|
|
46
|
+
total_tests += suite.test_cases.length
|
|
47
|
+
total_passed += suite.passed_count
|
|
48
|
+
total_failed += suite.failed_count
|
|
49
|
+
|
|
50
|
+
line = suite.name.ljust(max_suite_name_length + 1).bold + "\t"
|
|
51
|
+
line += "#{suite.passed_count.to_s.rjust(max_passed_length)} passed".green + "\t"
|
|
52
|
+
line += "#{suite.failed_count.to_s.rjust(max_failed_length)} failed".red
|
|
53
|
+
puts(line)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts
|
|
57
|
+
puts('Total tests: '.bold + total_tests.to_s.gray)
|
|
58
|
+
puts('Passed: '.green + total_passed.to_s.gray)
|
|
59
|
+
puts('Failed: '.red + (total_tests - total_passed).to_s.gray)
|
|
60
|
+
|
|
61
|
+
Process.exit(1) if total_failed > 0
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
|
|
4
|
+
class Slightish::Sandbox
|
|
5
|
+
attr_reader :path
|
|
6
|
+
|
|
7
|
+
def initialize(template_dir: nil, prefix: 'slightish')
|
|
8
|
+
@path = Dir.mktmpdir(prefix)
|
|
9
|
+
|
|
10
|
+
# The '.' prevents cp_r from making a new directory at the destination --
|
|
11
|
+
# kind of the equivalent of '/*' in bash.
|
|
12
|
+
FileUtils.cp_r(File.join(template_dir, '.'), @path) unless template_dir.nil?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def delete
|
|
16
|
+
FileUtils.remove_entry_secure(@path)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
class Slightish::TestCase
|
|
4
|
+
attr_reader :source_file
|
|
5
|
+
attr_accessor :start_line, :end_line
|
|
6
|
+
attr_reader :raw_command, :command
|
|
7
|
+
attr_reader :raw_expected_output, :expected_output
|
|
8
|
+
attr_reader :raw_expected_error_output, :expected_error_output
|
|
9
|
+
attr_accessor :expected_exit_code
|
|
10
|
+
attr_reader :actual_output, :actual_error_output, :actual_exit_code
|
|
11
|
+
|
|
12
|
+
def initialize(source_file)
|
|
13
|
+
@source_file = source_file
|
|
14
|
+
@expected_exit_code = 0
|
|
15
|
+
|
|
16
|
+
@start_line = @end_line = -1
|
|
17
|
+
@raw_command = @command = nil
|
|
18
|
+
@raw_expected_output = @expected_output = nil
|
|
19
|
+
@raw_expected_error_output = @expected_error_output = nil
|
|
20
|
+
@actual_output = @actual_error_output = nil
|
|
21
|
+
@actual_exit_code = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run(sandbox)
|
|
25
|
+
expand(sandbox)
|
|
26
|
+
|
|
27
|
+
@actual_output, @actual_error_output, process_status = Open3.capture3(@command, { chdir: sandbox.path })
|
|
28
|
+
@actual_output.chomp!
|
|
29
|
+
@actual_error_output.chomp!
|
|
30
|
+
@actual_exit_code = process_status.exitstatus
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def passed?
|
|
34
|
+
@actual_output == @expected_output &&
|
|
35
|
+
@actual_error_output == @expected_error_output &&
|
|
36
|
+
@actual_exit_code == @expected_exit_code
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def failed?
|
|
40
|
+
!passed?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def failure_description
|
|
44
|
+
res = ''
|
|
45
|
+
|
|
46
|
+
if @actual_output != (@expected_output || '')
|
|
47
|
+
if @expected_output.empty?
|
|
48
|
+
res += "Expected stdout: empty\n".red.bold
|
|
49
|
+
else
|
|
50
|
+
res += "Expected stdout:\n".red.bold
|
|
51
|
+
res += @expected_output.gray + "\n"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if @actual_output.empty?
|
|
55
|
+
res += 'Actual stdout: empty'.green.bold
|
|
56
|
+
else
|
|
57
|
+
res += "Actual stdout:\n".green.bold
|
|
58
|
+
res += @actual_output.gray
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if @actual_error_output != (@expected_error_output || '')
|
|
63
|
+
res += "\n\n" unless res == ''
|
|
64
|
+
if @expected_error_output.empty?
|
|
65
|
+
res += "Expected stderr: empty\n".red.bold
|
|
66
|
+
else
|
|
67
|
+
res += "Expected stderr:\n".red.bold
|
|
68
|
+
res += (@expected_error_output || '').gray + "\n"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if @actual_error_output.empty?
|
|
72
|
+
res += 'Actual stderr: empty'.green.bold
|
|
73
|
+
else
|
|
74
|
+
res += "Actual stderr:\n".green.bold
|
|
75
|
+
res += @actual_error_output.gray
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if @actual_exit_code != @expected_exit_code
|
|
80
|
+
res += "\n\n" unless res == ''
|
|
81
|
+
res += 'Expected exit code: '.red.bold + @expected_exit_code.to_s.gray + "\n"
|
|
82
|
+
res += 'Actual exit code: '.green.bold + @actual_exit_code.to_s.gray
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
res
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def source_description
|
|
89
|
+
if @start_line == @end_line
|
|
90
|
+
"#{@source_file}:@{@start_line}"
|
|
91
|
+
else
|
|
92
|
+
"#{@source_file}:#{@start_line}-#{@end_line}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def append_command(str)
|
|
97
|
+
if @raw_command.nil?
|
|
98
|
+
@raw_command = str
|
|
99
|
+
else
|
|
100
|
+
# bash eats newlines from multiline strings, so no \n here
|
|
101
|
+
# For example:
|
|
102
|
+
# "echo a\
|
|
103
|
+
# b"
|
|
104
|
+
# produces "ab"
|
|
105
|
+
@raw_command += str
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def append_expected_output(str)
|
|
110
|
+
if @raw_expected_output.nil?
|
|
111
|
+
@raw_expected_output = str
|
|
112
|
+
else
|
|
113
|
+
@raw_expected_output += "\n" + str
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def append_expected_error_output(str)
|
|
118
|
+
if @raw_expected_error_output.nil?
|
|
119
|
+
@raw_expected_error_output = str
|
|
120
|
+
else
|
|
121
|
+
@raw_expected_error_output += "\n" + str
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def expand(sandbox)
|
|
128
|
+
@command = @raw_command.expand(chdir: sandbox.path, source: source_description)
|
|
129
|
+
@expected_output = (@raw_expected_output || '').expand(chdir: sandbox.path, source: source_description)
|
|
130
|
+
@expected_error_output = (@raw_expected_error_output || '').expand(chdir: sandbox.path, source: source_description)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
require 'slightish/sandbox'
|
|
2
|
+
require 'slightish/test_case'
|
|
3
|
+
|
|
4
|
+
class Slightish::TestSuite
|
|
5
|
+
attr_reader :name, :path, :test_cases
|
|
6
|
+
attr_reader :sandbox_template_dir
|
|
7
|
+
|
|
8
|
+
def self.from_file(path, sandbox_template_dir: nil)
|
|
9
|
+
new(path, File.read(path), sandbox_template_dir: sandbox_template_dir)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(path, contents, sandbox_template_dir: nil)
|
|
13
|
+
@path = path
|
|
14
|
+
@name = File.basename(path)
|
|
15
|
+
@sandbox_template_dir = sandbox_template_dir
|
|
16
|
+
|
|
17
|
+
parse(path, contents)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
sandbox = Slightish::Sandbox.new(template_dir: @sandbox_template_dir)
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
@test_cases.each { |test| test.run(sandbox) }
|
|
25
|
+
ensure
|
|
26
|
+
sandbox.delete
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def failure_description
|
|
31
|
+
res = ''
|
|
32
|
+
@test_cases.select(&:failed?).each do |test|
|
|
33
|
+
res += "❌ #{test.source_description}\n".bold
|
|
34
|
+
res += test.failure_description + "\n\n"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
res
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def passed?
|
|
41
|
+
@test_cases.all?(&:passed?)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def failed?
|
|
45
|
+
@test_cases.any?(&:failed?)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def passed_count
|
|
49
|
+
@test_cases.count(&:passed?)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def failed_count
|
|
53
|
+
@test_cases.count(&:failed?)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
class ParseState
|
|
59
|
+
AWAITING_COMMAND = 0
|
|
60
|
+
READING_MULTILINE_COMMAND = 1
|
|
61
|
+
AWAITING_RESULT_OR_COMMAND = 2
|
|
62
|
+
AWAITING_STDERR_OR_EXIT_CODE_OR_COMMAND = 3
|
|
63
|
+
|
|
64
|
+
# Start in AWAITING_COMMAND
|
|
65
|
+
|
|
66
|
+
# AWAITING_COMMAND -> READING_MULTILINE_COMMAND on '$ .*\' (starting a new command)
|
|
67
|
+
# AWAITING_COMMAND -> AWAITING_RESULT_OR_COMMAND on '$ .*' (starting a new command)
|
|
68
|
+
# any other meaningful line on AWAITING_COMMAND is an error
|
|
69
|
+
|
|
70
|
+
# READING_MULTILINE_COMMAND -> READING_MULTILINE_COMMAND on '.*\'
|
|
71
|
+
# READING_MULTILINE_COMMAND -> AWAITING_RESULT_OR_COMMAND on anything else
|
|
72
|
+
# EOF from this state is error-ish, but eh, not worth it
|
|
73
|
+
|
|
74
|
+
# AWAITING_RESULT_OR_COMMAND -> AWAITING_RESULT_OR_COMMAND on '| .*'
|
|
75
|
+
# AWAITING_RESULT_OR_COMMAND -> AWAITING_COMMAND on '? \d+' (starting a new command)
|
|
76
|
+
# AWAITING_RESULT_OR_COMMAND -> AWAITING_STDERR_OR_EXIT_CODE_OR_COMMAND on '@ .*' (now accumulating stderr; stdout lines are no longer acceptable)
|
|
77
|
+
# inherits the AWAITING_COMMAND transitions
|
|
78
|
+
|
|
79
|
+
# AWAITING_STDERR_OR_EXIT_CODE_OR_COMMAND has same productions as AWAITING_RESULT_OR_COMMAND, except '| .*'
|
|
80
|
+
# '| .*' from here is an error
|
|
81
|
+
# EOF from here is fine; expected exit code = 0
|
|
82
|
+
|
|
83
|
+
# These are subsets of each other. So, we can test in this order and not repeat ourselves:
|
|
84
|
+
# READING_MULTILINE_COMMAND
|
|
85
|
+
# skip the line unless it begins with one of the magic strings
|
|
86
|
+
# AWAITING_RESULT_OR_COMMAND
|
|
87
|
+
# test for '| .*', else fall through
|
|
88
|
+
# AWAITING_RESULT_OR_COMMAND || AWAITING_STDERR_OR_EXIT_CODE_OR_COMMAND
|
|
89
|
+
# test for '@ .*' and '? \d+', else fall through
|
|
90
|
+
# AWAITING_COMMAND || AWAITING_RESULT_OR_COMMAND || AWAITING_STDERR_OR_EXIT_CODE_OR_COMMAND
|
|
91
|
+
# test for '$ .*\' or '$ .*'; anything else is an error
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse(file_name, file_contents)
|
|
95
|
+
@test_cases = []
|
|
96
|
+
|
|
97
|
+
current_case = nil
|
|
98
|
+
state = ParseState::AWAITING_COMMAND
|
|
99
|
+
|
|
100
|
+
file_contents.each_line.with_index(1) do |line, line_number|
|
|
101
|
+
if state == ParseState::READING_MULTILINE_COMMAND
|
|
102
|
+
if line =~ /^(?<cmd>.*)\\$/
|
|
103
|
+
# multiline input continues
|
|
104
|
+
current_case.append_command(Regexp.last_match(:cmd))
|
|
105
|
+
current_case.end_line = line_number
|
|
106
|
+
|
|
107
|
+
state = ParseState::READING_MULTILINE_COMMAND
|
|
108
|
+
else
|
|
109
|
+
# final line of multiline input; consume the whole thing
|
|
110
|
+
current_case.append_command(line.chomp)
|
|
111
|
+
current_case.end_line = line_number
|
|
112
|
+
|
|
113
|
+
state = ParseState::AWAITING_RESULT_OR_COMMAND
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
next
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Skip lines not intended for us
|
|
120
|
+
next unless line =~ /^[$|@?] /
|
|
121
|
+
|
|
122
|
+
if state == ParseState::AWAITING_RESULT_OR_COMMAND
|
|
123
|
+
if line =~ /^\| (?<output>.*)$/
|
|
124
|
+
# accumulating expected stdout
|
|
125
|
+
current_case.append_expected_output(Regexp.last_match(:output))
|
|
126
|
+
current_case.end_line = line_number
|
|
127
|
+
|
|
128
|
+
state = ParseState::AWAITING_RESULT_OR_COMMAND
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if [ParseState::AWAITING_RESULT_OR_COMMAND, ParseState::AWAITING_STDERR_OR_EXIT_CODE_OR_COMMAND].include?(state)
|
|
134
|
+
if line =~ /^@ (?<error_output>.*)$/
|
|
135
|
+
# accumulating expected stderr
|
|
136
|
+
current_case.append_expected_error_output(Regexp.last_match(:error_output))
|
|
137
|
+
current_case.end_line = line_number
|
|
138
|
+
|
|
139
|
+
state = ParseState::AWAITING_STDERR_OR_EXIT_CODE_OR_COMMAND
|
|
140
|
+
next
|
|
141
|
+
elsif line =~ /\? (?<exit_code>\d+)$/
|
|
142
|
+
# got exit code; only possible option from here is a new command
|
|
143
|
+
current_case.expected_exit_code = Regexp.last_match(:exit_code).to_i
|
|
144
|
+
current_case.end_line = line_number
|
|
145
|
+
|
|
146
|
+
state = ParseState::AWAITING_COMMAND
|
|
147
|
+
next
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# state is anything, and we are looking for a new command
|
|
152
|
+
unless line =~ /^\$ (?<cmd>.+?)(?<multiline>\\?)$/
|
|
153
|
+
raise SyntaxError, "invalid line in test file #{file_name}:#{line_number}; expected a '$ <command>' line"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
current_case = Slightish::TestCase.new(file_name)
|
|
157
|
+
current_case.start_line = current_case.end_line = line_number
|
|
158
|
+
@test_cases << current_case
|
|
159
|
+
|
|
160
|
+
current_case.append_command(Regexp.last_match(:cmd))
|
|
161
|
+
|
|
162
|
+
# entering multiline mode if we matched the slash
|
|
163
|
+
multiline = Regexp.last_match(:multiline) == '\\'
|
|
164
|
+
state = multiline ? ParseState::READING_MULTILINE_COMMAND : ParseState::AWAITING_RESULT_OR_COMMAND
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/slightish.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
class String
|
|
4
|
+
def self.color_output?
|
|
5
|
+
$stdout.isatty unless ENV['SLIGHTISH_NO_COLOR']
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
red: 1,
|
|
10
|
+
green: 2,
|
|
11
|
+
yellow: 3,
|
|
12
|
+
blue: 4,
|
|
13
|
+
gray: 7
|
|
14
|
+
}.each do |name, code|
|
|
15
|
+
bg_name = ('bg_' + name.to_s).to_sym
|
|
16
|
+
|
|
17
|
+
if color_output?
|
|
18
|
+
# :nocov:
|
|
19
|
+
define_method(name) { "\e[#{code + 30}m#{self}\e[39m" }
|
|
20
|
+
define_method(bg_name) { "\e[#{code + 40}m#{self}\e[49m" }
|
|
21
|
+
# :nocov:
|
|
22
|
+
else
|
|
23
|
+
define_method(name) { self }
|
|
24
|
+
define_method(bg_name) { self }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
{ bold: 1, faint: 2 }.each do |name, code|
|
|
29
|
+
if color_output?
|
|
30
|
+
# :nocov:
|
|
31
|
+
define_method(name) { "\e[#{code}m#{self}\e[22m" }
|
|
32
|
+
# :nocov:
|
|
33
|
+
else
|
|
34
|
+
define_method(name) { self }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def expand(chdir: nil, source: nil)
|
|
39
|
+
# Non-existent environmental variables are not replaced.
|
|
40
|
+
# A little unexpected, but it's the behavior of tush.
|
|
41
|
+
# TODO: print a warning when this happens?
|
|
42
|
+
variable_replacer = ->(match) { ENV.fetch(Regexp.last_match(:var_name), match) }
|
|
43
|
+
res = gsub(/\$(?<var_name>[[:alnum:]_]+)/, &variable_replacer) # $VARIABLE
|
|
44
|
+
res.gsub!(/\$\{(?<var_name>[[:alnum:]_]+)\}/, &variable_replacer) # ${VARIABLE}
|
|
45
|
+
|
|
46
|
+
command_replacer = ->(_) { capture_stdout_with_logging(Regexp.last_match(:cmd), chdir, source) }
|
|
47
|
+
res.gsub!(/\$\((?<cmd>[^\)]+)\)/, &command_replacer) # $(COMMAND)
|
|
48
|
+
res.gsub!(/`(?<cmd>[^`]+)`/, &command_replacer) # `COMMAND`
|
|
49
|
+
|
|
50
|
+
res
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def capture_stdout_with_logging(cmd, chdir, source)
|
|
56
|
+
if chdir.nil?
|
|
57
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
58
|
+
else
|
|
59
|
+
stdout, stderr, status = Open3.capture3(cmd, { chdir: chdir })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
unless stderr.empty?
|
|
63
|
+
message = 'warning: stderr from command substitution ('
|
|
64
|
+
message += source + '; ' unless source.nil? || source.empty?
|
|
65
|
+
message += "'#{cmd}') will be ignored"
|
|
66
|
+
$stderr.puts(message.yellow) unless ENV['SLIGHTISH_NO_WARNINGS']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless status.exitstatus.zero?
|
|
70
|
+
message = "warning: nonzero exit code (#{status.exitstatus}) from command substitution ("
|
|
71
|
+
message += source + '; ' unless source.nil? || source.empty?
|
|
72
|
+
message += "'#{cmd}')"
|
|
73
|
+
$stderr.puts(message.yellow) unless ENV['SLIGHTISH_NO_WARNINGS']
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
stdout.chomp
|
|
77
|
+
end
|
|
78
|
+
end
|
data/slightish.gemspec
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'slightish/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'slightish'
|
|
9
|
+
spec.version = Slightish::VERSION
|
|
10
|
+
spec.required_ruby_version = '>=2.2.1'
|
|
11
|
+
|
|
12
|
+
spec.authors = ['Tim Clem']
|
|
13
|
+
spec.email = ['tim.clem@gmail.com']
|
|
14
|
+
|
|
15
|
+
spec.summary = 'Literate testing of shell tools'
|
|
16
|
+
spec.description = %(
|
|
17
|
+
slightish lets you write and run tests for shell tools in a
|
|
18
|
+
simple, flexible syntax, intermingled with any other file
|
|
19
|
+
format (Markdown, plain text, HTML). Your documentation can
|
|
20
|
+
double as your tests.
|
|
21
|
+
).strip.gsub(/\s+/, ' ')
|
|
22
|
+
spec.homepage = 'http://github.com/misterfifths/slightish'
|
|
23
|
+
spec.license = 'MIT'
|
|
24
|
+
|
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
26
|
+
f.match(%r{^(test|spec|features)/})
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = 'exe'
|
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
30
|
+
spec.require_paths = ['lib']
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: slightish
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tim Clem
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-06-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: slightish lets you write and run tests for shell tools in a simple, flexible
|
|
14
|
+
syntax, intermingled with any other file format (Markdown, plain text, HTML). Your
|
|
15
|
+
documentation can double as your tests.
|
|
16
|
+
email:
|
|
17
|
+
- tim.clem@gmail.com
|
|
18
|
+
executables:
|
|
19
|
+
- slightish
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- ".gitignore"
|
|
24
|
+
- ".rubocop.yml"
|
|
25
|
+
- ".travis.yml"
|
|
26
|
+
- CODE_OF_CONDUCT.md
|
|
27
|
+
- Gemfile
|
|
28
|
+
- LICENSE.txt
|
|
29
|
+
- README.md
|
|
30
|
+
- Rakefile
|
|
31
|
+
- bin/console
|
|
32
|
+
- bin/setup
|
|
33
|
+
- exe/slightish
|
|
34
|
+
- lib/slightish.rb
|
|
35
|
+
- lib/slightish/command.rb
|
|
36
|
+
- lib/slightish/sandbox.rb
|
|
37
|
+
- lib/slightish/test_case.rb
|
|
38
|
+
- lib/slightish/test_suite.rb
|
|
39
|
+
- lib/slightish/version.rb
|
|
40
|
+
- lib/string_mixins.rb
|
|
41
|
+
- slightish.gemspec
|
|
42
|
+
homepage: http://github.com/misterfifths/slightish
|
|
43
|
+
licenses:
|
|
44
|
+
- MIT
|
|
45
|
+
metadata: {}
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 2.2.1
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubyforge_project:
|
|
62
|
+
rubygems_version: 2.4.6
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Literate testing of shell tools
|
|
66
|
+
test_files: []
|