command_line_boss 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,239 @@
1
+ Feature: CreateSpreadsheet::Cli#parse
2
+
3
+ Parse the create-spreadsheet command line
4
+
5
+ Example: Nothing is given on the command line
6
+ Given the command line ""
7
+ When the command line is parsed
8
+ Then the parser should have succeeded
9
+ And the title attribute should be nil
10
+ And the sheets attribute should be empty
11
+ And the permissions attribute should be empty
12
+ And the folder_id attribute should be nil
13
+
14
+ Example: A spreadsheet title is given
15
+ Given the command line "MySpreadsheet"
16
+ When the command line is parsed
17
+ Then the parser should have succeeded
18
+ And the title attribute should be "MySpreadsheet"
19
+
20
+ Example: A sheet is defined with a title
21
+ Given the command line "--sheet=Summary"
22
+ When the command line is parsed
23
+ Then the parser should have succeeded
24
+ And the sheets attribute should contain the following Sheets:
25
+ """
26
+ [
27
+ {
28
+ title: "Summary",
29
+ data: nil
30
+ }
31
+ ]
32
+ """
33
+
34
+ Example: A sheet is defined with a title and data
35
+ Given the command line "--sheet=Summary --data=data1.csv"
36
+ And a file "data1.csv" containing:
37
+ """
38
+ 1,2,3
39
+ 4,5,6
40
+ """
41
+ When the command line is parsed
42
+ Then the parser should have succeeded
43
+ And the sheets attribute should contain the following Sheets:
44
+ """
45
+ [
46
+ {
47
+ "title": "Summary",
48
+ "data": [["1", "2", "3"], ["4", "5", "6"]]
49
+ }
50
+ ]
51
+ """
52
+
53
+ Example: Multiple sheets are defined both with data
54
+ Given the command line "--sheet=Summary --data=summary.csv --sheet=Detail --data=detail.csv"
55
+ And a file "summary.csv" containing:
56
+ """
57
+ 6
58
+ """
59
+ And a file "detail.csv" containing:
60
+ """
61
+ 1
62
+ 2
63
+ 3
64
+ """
65
+ When the command line is parsed
66
+ Then the parser should have succeeded
67
+ And the sheets attribute should contain the following Sheets:
68
+ """
69
+ [
70
+ { "title": "Summary", "data": [["6"]] },
71
+ { "title": "Detail", "data": [["1"], ["2"], ["3"]] }
72
+ ]
73
+ """
74
+
75
+ Example: Multiple sheets are defined only one with data
76
+ Given the command line "--sheet=Summary --sheet=Detail --data=detail.csv"
77
+ And a file "detail.csv" containing:
78
+ """
79
+ Name,Age
80
+ John,25
81
+ Jane,23
82
+ """
83
+ When the command line is parsed
84
+ Then the parser should have succeeded
85
+ And the sheets attribute should contain the following Sheets:
86
+ """
87
+ [
88
+ { title: "Summary", data: nil },
89
+ { title: "Detail", data: [["Name", "Age"], ["John", "25"], ["Jane", "23"]] }
90
+ ]
91
+ """
92
+
93
+ Example: A user permission is given
94
+ Given the command line "--permission=user:bob@example.com:reader"
95
+ When the command line is parsed
96
+ Then the parser should have succeeded
97
+ And the permissions attribute should contain the following Permissions:
98
+ """
99
+ [
100
+ {
101
+ "permission_spec": "user:bob@example.com:reader",
102
+ "type": "user", "subject": "bob@example.com", "role": "reader"
103
+ }
104
+ ]
105
+ """
106
+
107
+ Example: A group permission is given
108
+ Given the command line "--permission=group:admins@example.com:writer"
109
+
110
+ When the command line is parsed
111
+ Then the parser should have succeeded
112
+ And the permissions attribute should contain the following Permissions:
113
+ """
114
+ [
115
+ {
116
+ "permission_spec": "group:admins@example.com:writer",
117
+ "type": "group", "subject": "admins@example.com", "role": "writer"
118
+ }
119
+ ]
120
+ """
121
+
122
+ Example: A domain permission is given
123
+ Given the command line "--permission=domain:domain_name:reader"
124
+ When the command line is parsed
125
+ Then the parser should have succeeded
126
+ And the permissions attribute should contain the following Permissions:
127
+ """
128
+ [
129
+ {
130
+ "permission_spec": "domain:domain_name:reader",
131
+ "type": "domain", "subject": "domain_name", "role": "reader"
132
+ }
133
+ ]
134
+ """
135
+
136
+ Example: An anyone permission is given
137
+ Given the command line "--permission=anyone:reader"
138
+ When the command line is parsed
139
+ Then the parser should have succeeded
140
+ And the permissions attribute should contain the following Permissions:
141
+ """
142
+ [
143
+ {
144
+ permission_spec: "anyone:reader", type: "anyone", subject: nil, role: "reader"
145
+ }
146
+ ]
147
+ """
148
+
149
+ Example: Multiple permissions are given
150
+ Given the command line "--permission=user:bob@example.com:writer --permission=anyone:reader"
151
+ When the command line is parsed
152
+ Then the parser should have succeeded
153
+ And the permissions attribute should contain the following Permissions:
154
+ """
155
+ [
156
+ {
157
+ permission_spec: "user:bob@example.com:writer", type: "user", subject: "bob@example.com", role: "writer"
158
+ },
159
+ {
160
+ permission_spec: "anyone:reader", type: "anyone", subject: nil, role: "reader"
161
+ }
162
+ ]
163
+ """
164
+
165
+ Example: A folder is given
166
+ Given the command line "--folder=0ALLuhm2AwwlJUk9PVA"
167
+ When the command line is parsed
168
+ Then the parser should have succeeded
169
+ And the folder_id attribute should be "0ALLuhm2AwwlJUk9PVA"
170
+
171
+ # Failure cases
172
+
173
+ Example: A sheet given without a name
174
+ Given the command line "--sheet"
175
+ When the command line is parsed
176
+ Then the parser should have failed with the error "ERROR: missing argument: --sheet"
177
+
178
+ Example: A permission is given without a permission spec
179
+ Given the command line "--permission"
180
+ When the command line is parsed
181
+ Then the parser should have failed with the error "ERROR: missing argument: --permission"
182
+
183
+ Example: A invalid permission is given
184
+ Given the command line "--permission=anyone-writer"
185
+ When the command line is parsed
186
+ Then the parser should have failed with the error "ERROR: Invalid permission: anyone-writer"
187
+
188
+ Example: The permission spec has an invalid type
189
+ Given the command line "--permission=invalid:test@example.com:reader"
190
+ When the command line is parsed
191
+ Then the parser should have failed with the error "ERROR: Invalid permission type: invalid"
192
+
193
+ Example: A permission spec has an invalid role
194
+ Given the command line "--permission=user:test@example.com:invalid"
195
+ When the command line is parsed
196
+ Then the parser should have failed with the error "ERROR: Invalid permission role: invalid"
197
+
198
+ Example: A subject is given for an anyone permission
199
+ Given the command line "--permission=anyone:test@example.com:reader"
200
+ When the command line is parsed
201
+ Then the parser should have failed with the error "ERROR: An anyone permission must not have a subject"
202
+
203
+ Example: A subject is not given for a user permission
204
+ Given the command line "--permission=user:writer"
205
+ When the command line is parsed
206
+ Then the parser should have failed with the error "ERROR: A user permission must have a subject"
207
+
208
+ Example: Data is given without a path
209
+ Given the command line "--sheet=Summary --data"
210
+ When the command line is parsed
211
+ Then the parser should have failed with the error "ERROR: missing argument: --data"
212
+
213
+ Example: Data is given with an non-existant path
214
+ Given the command line "--sheet=Summary --data=nonexistent.csv"
215
+ And the file "nonexistent.csv" does not exist
216
+ When the command line is parsed
217
+ Then the parser should have failed with the error "ERROR: Data file not found: nonexistent.csv"
218
+
219
+ Example: The folder option is given twice
220
+ Given the command line "--folder=0ALLuhm2AwwlJUk9PVA --folder=0ALLuhm2AwwlJUk9PVA"
221
+ When the command line is parsed
222
+
223
+ Example: A same sheet name is given twice
224
+ Given the command line "--sheet=Summary --sheet=Summary"
225
+ When the command line is parsed
226
+ Then the parser should have failed with the error "ERROR: The sheet Summary was given more than once"
227
+
228
+ Example: Data is given twice for the same sheet
229
+ Given the command line "--sheet=Summary --data=data1.csv --data=data2.csv"
230
+ And a file "data1.csv" containing:
231
+ """
232
+ 1
233
+ """
234
+ And a file "data2.csv" containing:
235
+ """
236
+ 2
237
+ """
238
+ When the command line is parsed
239
+ Then the parser should have failed with the error "ERROR: Only one data file is allowed per sheet"
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ step 'the command line :command_line' do |command_line|
4
+ @args = command_line.split
5
+ end
6
+
7
+ # step 'the command line is parsed' do
8
+ # @options = CreateSpreadsheetCli.new.parse(@args)
9
+ # end
10
+
11
+ step 'the command line is parsed' do
12
+ stdout = StringIO.new
13
+ stderr = StringIO.new
14
+ original_stdout = $stdout
15
+ original_stderr = $stderr
16
+
17
+ begin
18
+ $stdout = stdout
19
+ $stderr = stderr
20
+ @options = CreateSpreadsheet::CommandLine.new.parse(@args)
21
+ rescue StandardError => e
22
+ @error = e
23
+ @options = e.parser
24
+ ensure
25
+ $stdout = original_stdout
26
+ $stderr = original_stderr
27
+ end
28
+
29
+ @stdout = stdout.string
30
+ @stderr = stderr.string
31
+ end
32
+
33
+ step 'the parser should have succeeded' do
34
+ expect(@options.error_messages).to eq([])
35
+ expect(@error).to be_nil
36
+ expect(@stdout).to be_empty
37
+ expect(@stderr).to be_empty
38
+ end
39
+
40
+ step 'the parser should have failed' do
41
+ expect(@options.error_messages).not_to be_empty
42
+ expect(@error).to be_nil
43
+ expect(@stdout).to be_empty
44
+ expect(@stderr).to be_empty
45
+ end
46
+
47
+ step 'the parser should have failed with the error :message' do |message|
48
+ expect(@options.error_messages).not_to be_empty
49
+ expect(@error).to be_nil
50
+ expect(@stdout).to be_empty
51
+ expect(@stderr).to be_empty
52
+ expect(@options.error_messages).to include(message)
53
+ end
54
+
55
+ step 'the :attribute attribute should be nil' do |attribute|
56
+ expect(@options.send(attribute.to_sym)).to be_nil
57
+ end
58
+
59
+ step 'the :attribute attribute should be empty' do |attribute|
60
+ expect(@options.send(attribute.to_sym)).to be_empty
61
+ end
62
+
63
+ step 'the :attribute attribute should be ":value"' do |attribute, value|
64
+ expect(@options.send(attribute.to_sym)).to eq(value)
65
+ end
66
+
67
+ step 'the sheets attribute should contain the following Sheets:' do |hash_as_string|
68
+ # rubocop:disable Security/Eval
69
+ expected_sheets = eval(hash_as_string).map { |s| CreateSpreadsheet::Sheet.new(s) }
70
+ # rubocop:enable Security/Eval
71
+ expect(@options.sheets).to match_array(expected_sheets)
72
+ end
73
+
74
+ RSpec.configure do |config|
75
+ config.before(type: :feature) do
76
+ allow(File).to receive(:read).and_call_original
77
+ end
78
+ end
79
+
80
+ step 'a file :path containing:' do |path, content|
81
+ allow(File).to receive(:read).with(path).and_return(content)
82
+ end
83
+
84
+ step 'the file :path does not exist' do |path|
85
+ allow(File).to receive(:read).with(path).and_raise(Errno::ENOENT, "No such file or directory @ rb_sysopen - #{path}")
86
+ end
87
+
88
+ step 'the permissions attribute should contain the following Permissions:' do |hash_as_string|
89
+ # rubocop:disable Security/Eval
90
+ expected_permissions = eval(hash_as_string).map { |p| CreateSpreadsheet::Permission.new(p) }
91
+ # rubocop:enable Security/Eval
92
+ expect(@options.permissions).to match_array(expected_permissions)
93
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Turnip setup
4
+ #
5
+ require 'turnip/rspec'
6
+ Dir[File.join(__dir__, '**/*_steps.rb')].each { |f| require f }
7
+
8
+ RSpec.configure do |config|
9
+ # Enable flags like --only-failures and --next-failure
10
+ config.example_status_persistence_file_path = '.rspec_status'
11
+
12
+ # Disable RSpec exposing methods globally on `Module` and `main`
13
+ config.disable_monkey_patching!
14
+
15
+ config.expect_with :rspec do |c|
16
+ c.syntax = :expect
17
+ end
18
+ end
19
+
20
+ # Setup simplecov
21
+
22
+ require 'simplecov'
23
+ require 'simplecov-lcov'
24
+ require 'json'
25
+
26
+ SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::LcovFormatter]
27
+
28
+ # Return `true` if the environment variable is set to a truthy value
29
+ #
30
+ # @example
31
+ # env_true?('COV_SHOW_UNCOVERED')
32
+ #
33
+ # @param name [String] the name of the environment variable
34
+ # @return [Boolean]
35
+ #
36
+ def env_true?(name)
37
+ value = ENV.fetch(name, '').downcase
38
+ %w[yes on true 1].include?(value)
39
+ end
40
+
41
+ # Return `true` if the environment variable is NOT set to a truthy value
42
+ #
43
+ # @example
44
+ # env_false?('COV_NO_FAIL')
45
+ #
46
+ # @param name [String] the name of the environment variable
47
+ # @return [Boolean]
48
+ #
49
+ def env_false?(name)
50
+ !env_true?(name)
51
+ end
52
+
53
+ # Return `true` if the the test run should fail if the coverage is below the threshold
54
+ #
55
+ # @return [Boolean]
56
+ #
57
+ def fail_on_low_coverage?
58
+ !(RSpec.configuration.dry_run? || env_true?('COV_NO_FAIL'))
59
+ end
60
+
61
+ # Return `true` if the the test run should show the lines not covered by tests
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ def show_lines_not_covered?
66
+ env_true?('COV_SHOW_UNCOVERED')
67
+ end
68
+
69
+ # Report if the test coverage was below the configured threshold
70
+ #
71
+ # The threshold is configured by setting the `test_coverage_threshold` variable
72
+ # in this file.
73
+ #
74
+ # Example:
75
+ #
76
+ # ```Ruby
77
+ # test_coverage_threshold = 100
78
+ # ```
79
+ #
80
+ # Coverage below the threshold will cause the rspec run to fail unless the
81
+ # `COV_NO_FAIL` environment variable is set to TRUE.
82
+ #
83
+ # ```Shell
84
+ # COV_NO_FAIL=TRUE rspec
85
+ # ```
86
+ #
87
+ # Example of running the tests in an infinite loop writing failures to `fail.txt`:
88
+ #
89
+ # ```Shell
90
+ # while true; do COV_NO_FAIL=TRUE rspec >> fail.txt; done
91
+ # ````
92
+ #
93
+ # The lines missing coverage will be displayed if the `COV_SHOW_UNCOVERED`
94
+ # environment variable is set to TRUE.
95
+ #
96
+ # ```Shell
97
+ # COV_SHOW_UNCOVERED=TRUE rspec
98
+ # ```
99
+ #
100
+ test_coverage_threshold = 100
101
+
102
+ SimpleCov.at_exit do
103
+ SimpleCov.result.format!
104
+ # rubocop:disable Style/StderrPuts
105
+ if SimpleCov.result.covered_percent < test_coverage_threshold
106
+ $stderr.puts
107
+ $stderr.print 'FAIL: ' if fail_on_low_coverage?
108
+ $stderr.puts "RSpec Test coverage fell below #{test_coverage_threshold}%"
109
+
110
+ if show_lines_not_covered?
111
+ $stderr.puts "\nThe following lines were not covered by tests:\n"
112
+ SimpleCov.result.files.each do |source_file| # SimpleCov::SourceFile
113
+ source_file.missed_lines.each do |line| # SimpleCov::SourceFile::Line
114
+ $stderr.puts " .#{source_file.project_filename}:#{line.number}"
115
+ end
116
+ end
117
+ end
118
+
119
+ $stderr.puts
120
+
121
+ exit 1 if fail_on_low_coverage?
122
+ end
123
+ # rubocop:enable Style/StderrPuts
124
+ end
125
+
126
+ SimpleCov.start do
127
+ enable_coverage :branch
128
+ end
129
+
130
+ require 'create_spreadsheet'
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'command_line_boss'
5
+
6
+ # Define a command line interface for creating a spreadsheet
7
+ class CreateSpreadsheetCli < CommandLineBoss
8
+ attr_reader :spreadsheet_name, :sheet_names
9
+
10
+ private
11
+
12
+ def set_defaults
13
+ @spreadsheet_name = nil
14
+ @sheet_names = []
15
+ end
16
+
17
+ def define_sheet_option
18
+ parser.on('--sheet=SHEET_NAME', 'Name of a sheet to create') do |name|
19
+ add_error_message('Sheet names must be unique!') if sheet_names.include?(name)
20
+ sheet_names << name
21
+ end
22
+ end
23
+
24
+ def validate_spreadsheet_name_given
25
+ add_error_message('A spreadsheet name is required') if spreadsheet_name.nil?
26
+ end
27
+
28
+ def validate_at_least_one_sheet_name_given
29
+ add_error_message('At least one sheet name is required') if sheet_names.empty?
30
+ end
31
+
32
+ def parse_arguments
33
+ @spreadsheet_name = args.shift
34
+ end
35
+
36
+ include CommandLineBoss::HelpOption
37
+
38
+ def banner = <<~BANNER
39
+ Create a spreadsheetasdf
40
+
41
+ Usage:
42
+ create_spreadsheet SPREADSHEET_NAME --sheet=SHEET_NAME [--sheet=SHEET_NAME ...]
43
+
44
+ BANNER
45
+ end
46
+
47
+ # Parse the command line arguments
48
+
49
+ options = CreateSpreadsheetCli.new.parse(ARGV)
50
+
51
+ # Report errors
52
+
53
+ if options.failed?
54
+ warn options.error_messages.join("\n")
55
+ exit 1
56
+ end
57
+
58
+ # Do something with the result
59
+
60
+ require 'pp'
61
+
62
+ puts \
63
+ "Creating spreadsheet #{options.spreadsheet_name.pretty_inspect.chomp} " \
64
+ "with sheets #{options.sheet_names.map(&:pretty_inspect).map(&:chomp).join(', ')}"
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CommandLineBoss
4
+ # Add the --help option
5
+ module HelpOption
6
+ private
7
+
8
+ # Define the command line options
9
+ #
10
+ # @return [void]
11
+ #
12
+ # @api private
13
+ #
14
+ def define_options
15
+ add_banner
16
+ add_header
17
+ super
18
+ add_footer
19
+ end
20
+
21
+ # Define the --help option
22
+ #
23
+ # @return [void]
24
+ #
25
+ # @api private
26
+ #
27
+ def define_help_option
28
+ parser.on('-h', '--help', 'Show this message') do
29
+ puts parser.help
30
+ exit
31
+ end
32
+ end
33
+
34
+ # Adds the banner to the parser output
35
+ # @return [void]
36
+ # @api private
37
+ #
38
+ def add_banner
39
+ return unless banner
40
+
41
+ parser.banner = banner
42
+ end
43
+
44
+ # Derived classes should override this method to provide a banner
45
+ # @return [String, nil]
46
+ # @api private
47
+ #
48
+ def banner = nil
49
+
50
+ # Adds the header to the parser output
51
+ # @return [void]
52
+ # @api private
53
+ #
54
+ def add_header
55
+ return unless header
56
+
57
+ parser.separator header
58
+ end
59
+
60
+ # Derived classes should override this method to provide a header
61
+ # @return [String, nil]
62
+ # @api private
63
+ #
64
+ def header = nil
65
+
66
+ # Adds the footer to the parser output
67
+ # @return [void]
68
+ # @api private
69
+ #
70
+ def add_footer
71
+ return unless footer
72
+
73
+ parser.separator footer
74
+ end
75
+
76
+ # Derived classes should override this method to provide a footer
77
+ # @return [String, nil]
78
+ # @api private
79
+ #
80
+ def footer = nil
81
+ end
82
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CommandLineBoss
4
+ # Add --debug and --verbose options and a logger method
5
+ module LoggerOptions
6
+ # true if the --debug option was given
7
+ #
8
+ # @return [Boolean]
9
+ #
10
+ # @api private
11
+ #
12
+ attr_reader :debug
13
+
14
+ # true if the --verbose option was given
15
+ #
16
+ # @return [Boolean]
17
+ #
18
+ # @api private
19
+ #
20
+ attr_reader :verbose
21
+
22
+ # The logger to use to report progress
23
+ #
24
+ # Messages are logged at info and debug levels. The logger returned is one of
25
+ # the following:
26
+ #
27
+ # * A logger that logs to the console at the :info level if verbose mode is enabled
28
+ # * A logger that logs to the console at the :debug level if debug mode is enabled
29
+ # * Otherwise a null logger that does not log anything
30
+ #
31
+ # @example
32
+ # options.logger #=> #<Logger:0x00007f9e3b8b3e08>
33
+ #
34
+ # @return [Logger]
35
+ #
36
+ def logger
37
+ @logger ||=
38
+ if verbose
39
+ verbose_logger
40
+ elsif debug
41
+ debug_logger
42
+ else
43
+ Logger.new(nil, level: Logger::UNKNOWN + 1)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Define the --verbose option
50
+ #
51
+ # @return [void]
52
+ #
53
+ # @api private
54
+ #
55
+ def define_verbose_option
56
+ parser.on('-v', '--verbose', 'Enable verbose mode (default is off)') do |verbose|
57
+ @verbose = verbose
58
+ end
59
+ end
60
+
61
+ # Define the --debug option
62
+ #
63
+ # @return [void]
64
+ #
65
+ # @api private
66
+ #
67
+ def define_debug_option
68
+ parser.on('-D', '--debug', 'Enable debug mode default is off') do |debug|
69
+ @debug = debug
70
+ end
71
+ end
72
+
73
+ # Ensure that the --debug and --verbose options are not both given
74
+ #
75
+ # @return [void]
76
+ #
77
+ # @api private
78
+ #
79
+ def validate_debug_verbose_option
80
+ add_error_message('Can not give both --debug and --verbose') if debug && verbose
81
+ end
82
+
83
+ # A Logger that logs to the console at the :debug level with a simple formatter
84
+ #
85
+ # @return [Logger]
86
+ #
87
+ # @api private
88
+ #
89
+ def debug_logger
90
+ Logger.new($stdout, level: 'debug', formatter: ->(_severity, _datetime, _progname, msg) { "#{msg}\n" })
91
+ end
92
+
93
+ # A Logger that logs to the console at the :info level with a simple formatter
94
+ #
95
+ # @return [Logger]
96
+ #
97
+ # @api private
98
+ #
99
+ def verbose_logger
100
+ Logger.new($stdout, level: 'info', formatter: ->(_severity, _datetime, _progname, msg) { "#{msg}\n" })
101
+ end
102
+ end
103
+ end