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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
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
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.4.0
5
+ - 2.2.1
6
+
7
+ script:
8
+ - bundle exec rake test
@@ -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
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in slightish.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'bundler'
8
+ gem 'minitest'
9
+ gem 'rake'
10
+ gem 'rubocop'
11
+ gem 'simplecov'
12
+ gem 'coveralls'
13
+ end
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
+ [![Build Status](https://travis-ci.org/misterfifths/slightish.svg?branch=master)](https://travis-ci.org/misterfifths/slightish) [![Coverage Status](https://coveralls.io/repos/github/misterfifths/slightish/badge.svg?branch=master)](https://coveralls.io/github/misterfifths/slightish?branch=master) [![npm](https://img.shields.io/gem/v/slightish.svg)](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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/slightish ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'slightish'
4
+
5
+ Slightish::Command.run(ARGV)
@@ -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
@@ -0,0 +1,3 @@
1
+ module Slightish
2
+ VERSION = '0.1.0'.freeze
3
+ end
data/lib/slightish.rb ADDED
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'slightish/version'
4
+
5
+ require 'string_mixins'
6
+
7
+ require 'slightish/sandbox'
8
+ require 'slightish/test_case'
9
+ require 'slightish/test_suite'
10
+ require 'slightish/command'
@@ -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: []