howzit 2.1.40 → 2.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/bin/howzit +2 -0
- data/lib/howzit/condition_evaluator.rb +11 -0
- data/lib/howzit/topic.rb +56 -32
- data/lib/howzit/util.rb +12 -1
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +1 -1
- data/spec/condition_evaluator_spec.rb +24 -0
- data/spec/sequential_conditional_spec.rb +49 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/topic_positional_snapshot_spec.rb +33 -0
- data/spec/topic_spec.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 30837683e82b813fd1fd8ff3e2df3177cbee3c735fc5afb2cb3394f08fb341eb
|
|
4
|
+
data.tar.gz: 4a0113424a3e36169a0f7a66f31b6dcc3adc24785d9a1a54829db7d9d907c585
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c0245bdb6e801d1efb96e800c04a7ac1177a589e9d25753322bb70a6690ce22b738fdfdc12956aae216565e93ea23b7cdcc14b6f278937e329b116b8d40b5cd
|
|
7
|
+
data.tar.gz: 986c7c270fc4d8aa21c395fab7a1edaa5669c4a72e6160dede78e45c9c9b6065e69ca47c6c4253b1eae1ec2909d4e31224dee333ba74e6b392e937fe1f7f25ae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
### 2.1.41
|
|
2
|
+
|
|
3
|
+
2026-05-10 08:18
|
|
4
|
+
|
|
5
|
+
#### NEW
|
|
6
|
+
|
|
7
|
+
- `-z name` and `-n name` in `@if`/`@unless` conditions for shell-style empty and non-empty checks on named and positional variables.
|
|
8
|
+
|
|
9
|
+
#### IMPROVED
|
|
10
|
+
|
|
11
|
+
- `Util.show` strips safely when the default external encoding rejects multibyte content (fewer crashes under strict ASCII locale when piping or highlighting).
|
|
12
|
+
|
|
13
|
+
#### FIXED
|
|
14
|
+
|
|
15
|
+
- @set_var and @log_level inside @if/@else/@elsif so only the active branch runs; the previous behavior ran every branch and could overwrite variables set in another branch (for example ONLY_RUN set in @if then replaced by @else).
|
|
16
|
+
- Topic title parameters from arguments after `--` so they no longer shift when another topic is parsed first with `@include(...[args])` (CLI positional snapshot kept separate from mutable Howzit.arguments during load).
|
|
17
|
+
|
|
1
18
|
### 2.1.40
|
|
2
19
|
|
|
3
20
|
2026-02-04 07:53
|
data/bin/howzit
CHANGED
|
@@ -9,6 +9,8 @@ Howzit::Color.coloring = $stdout.isatty
|
|
|
9
9
|
parts = Shellwords.shelljoin(ARGV).split(/ -- /)
|
|
10
10
|
args = parts[0] ? Shellwords.shellsplit(parts[0]) : []
|
|
11
11
|
Howzit.arguments = parts[1] ? Shellwords.shellsplit(parts[1]) : []
|
|
12
|
+
# Immutable snapshot for topic title (param): binding — gather_tasks/@include mutates Howzit.arguments while parsing earlier topics
|
|
13
|
+
Howzit.cli_topic_positional_args = Howzit.arguments&.dup || []
|
|
12
14
|
Howzit.named_arguments = {}
|
|
13
15
|
|
|
14
16
|
OptionParser.new do |opts|
|
|
@@ -35,6 +35,17 @@ module Howzit
|
|
|
35
35
|
## Evaluate a single condition (without negation)
|
|
36
36
|
##
|
|
37
37
|
def evaluate_condition(condition, context)
|
|
38
|
+
# Shell-style empty / non-empty tests (bash -z / -n)
|
|
39
|
+
if (match = condition.match(/^-z\s+(.+)$/i))
|
|
40
|
+
name = match[1].strip
|
|
41
|
+
val = get_value(name, context)
|
|
42
|
+
return val.nil? || val.to_s.strip.empty?
|
|
43
|
+
elsif (match = condition.match(/^-n\s+(.+)$/i))
|
|
44
|
+
name = match[1].strip
|
|
45
|
+
val = get_value(name, context)
|
|
46
|
+
return !val.nil? && !val.to_s.strip.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
38
49
|
# Handle special conditions FIRST to avoid false matches with comparison patterns
|
|
39
50
|
# Check file contents before other patterns since it has arguments and operators
|
|
40
51
|
if condition =~ /^file\s+contents\s+(.+?)\s+(\*\*=|\*=|\^=|\$=|==|!=|=~)\s*(.+)$/i
|
data/lib/howzit/topic.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Howzit
|
|
|
25
25
|
@named_args = {}
|
|
26
26
|
@metadata = metadata
|
|
27
27
|
@source_file = source_file
|
|
28
|
-
arguments
|
|
28
|
+
arguments(from_cli_snapshot: true)
|
|
29
29
|
|
|
30
30
|
@directives = parse_directives_with_conditionals
|
|
31
31
|
@tasks = gather_tasks
|
|
@@ -33,10 +33,23 @@ module Howzit
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Get named arguments from title
|
|
36
|
-
|
|
36
|
+
# from_cli_snapshot: use Howzit.cli_topic_positional_args (set once from argv after ` -- `) so earlier topics' gather_tasks
|
|
37
|
+
# cannot clobber positional binding. Re-entrant calls (e.g. @include with [a,b]) pass false to use live Howzit.arguments.
|
|
38
|
+
def arguments(from_cli_snapshot: false)
|
|
37
39
|
@arg_definitions = []
|
|
38
40
|
return unless @title =~ /\(.*?\) *$/
|
|
39
41
|
|
|
42
|
+
positional = if from_cli_snapshot
|
|
43
|
+
# Specs / non-CLI: leave unset to keep using Howzit.arguments
|
|
44
|
+
if Howzit.cli_topic_positional_args.nil?
|
|
45
|
+
Howzit.arguments || []
|
|
46
|
+
else
|
|
47
|
+
Howzit.cli_topic_positional_args
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
Howzit.arguments || []
|
|
51
|
+
end
|
|
52
|
+
|
|
40
53
|
a = @title.match(/\((?<args>.*?)\) *$/)
|
|
41
54
|
args = a['args'].split(/ *, */).each(&:strip)
|
|
42
55
|
|
|
@@ -45,8 +58,8 @@ module Howzit
|
|
|
45
58
|
# Store original definition for display purposes
|
|
46
59
|
@arg_definitions << (default ? "#{arg_name}:#{default}" : arg_name)
|
|
47
60
|
|
|
48
|
-
@named_args[arg_name] = if
|
|
49
|
-
|
|
61
|
+
@named_args[arg_name] = if positional && positional.count >= idx + 1
|
|
62
|
+
positional[idx]
|
|
50
63
|
else
|
|
51
64
|
default
|
|
52
65
|
end
|
|
@@ -642,6 +655,7 @@ module Howzit
|
|
|
642
655
|
if line =~ /^@log_level\s*\(([^)]+)\)\s*$/i
|
|
643
656
|
log_level = Regexp.last_match(1).strip
|
|
644
657
|
conditional_path = conditional_stack.dup
|
|
658
|
+
conditional_path << current_branch_index if current_branch_index
|
|
645
659
|
directives << Howzit::Directive.new(
|
|
646
660
|
type: :log_level,
|
|
647
661
|
log_level_value: log_level,
|
|
@@ -664,6 +678,7 @@ module Howzit
|
|
|
664
678
|
# Remove quotes from value if present (handles both single and double quotes)
|
|
665
679
|
var_value = Regexp.last_match(1) if var_value =~ /^["'](.+)["']$/
|
|
666
680
|
conditional_path = conditional_stack.dup
|
|
681
|
+
conditional_path << current_branch_index if current_branch_index
|
|
667
682
|
directives << Howzit::Directive.new(
|
|
668
683
|
type: :set_var,
|
|
669
684
|
var_name: var_name,
|
|
@@ -818,12 +833,16 @@ module Howzit
|
|
|
818
833
|
|
|
819
834
|
# Handle @log_level directive (before task check)
|
|
820
835
|
if directive.log_level?
|
|
836
|
+
next unless directive_in_active_branch?(directive, conditional_state)
|
|
837
|
+
|
|
821
838
|
current_log_level = directive.log_level_value
|
|
822
839
|
next
|
|
823
840
|
end
|
|
824
841
|
|
|
825
842
|
# Handle @set_var directive (before task check)
|
|
826
843
|
if directive.set_var?
|
|
844
|
+
next unless directive_in_active_branch?(directive, conditional_state)
|
|
845
|
+
|
|
827
846
|
# Set the variable in named_arguments
|
|
828
847
|
Howzit.named_arguments ||= {}
|
|
829
848
|
value = directive.var_value
|
|
@@ -856,34 +875,7 @@ module Howzit
|
|
|
856
875
|
# Handle task directives
|
|
857
876
|
next unless directive.task?
|
|
858
877
|
|
|
859
|
-
|
|
860
|
-
should_execute = true
|
|
861
|
-
|
|
862
|
-
# If path ends with an @elsif/@else, skip the parent @if index
|
|
863
|
-
# (the index right before the elsif/else in the path)
|
|
864
|
-
path_to_check = directive.conditional_path.dup
|
|
865
|
-
if path_to_check.length >= 2
|
|
866
|
-
last_idx = path_to_check.last
|
|
867
|
-
last_state = conditional_state[last_idx]
|
|
868
|
-
if last_state && %w[elsif else].include?(last_state[:directive_type])
|
|
869
|
-
# Skip the parent @if index (the one before the elsif/else)
|
|
870
|
-
parent_if_idx = path_to_check[path_to_check.length - 2]
|
|
871
|
-
parent_if_state = conditional_state[parent_if_idx]
|
|
872
|
-
if parent_if_state && %w[if unless].include?(parent_if_state[:directive_type])
|
|
873
|
-
path_to_check.delete(parent_if_idx)
|
|
874
|
-
end
|
|
875
|
-
end
|
|
876
|
-
end
|
|
877
|
-
|
|
878
|
-
path_to_check.each do |cond_idx|
|
|
879
|
-
cond_state = conditional_state[cond_idx]
|
|
880
|
-
if cond_state.nil? || !cond_state[:evaluated] || !cond_state[:result]
|
|
881
|
-
should_execute = false
|
|
882
|
-
break
|
|
883
|
-
end
|
|
884
|
-
end
|
|
885
|
-
|
|
886
|
-
next unless should_execute
|
|
878
|
+
next unless directive_in_active_branch?(directive, conditional_state)
|
|
887
879
|
|
|
888
880
|
# Convert directive to task
|
|
889
881
|
task = directive.to_task(self, current_log_level: current_log_level)
|
|
@@ -939,6 +931,38 @@ module Howzit
|
|
|
939
931
|
output
|
|
940
932
|
end
|
|
941
933
|
|
|
934
|
+
##
|
|
935
|
+
## Whether a directive nested under @if/@unless/@elsif/@else should run (same rules as tasks).
|
|
936
|
+
##
|
|
937
|
+
def directive_in_active_branch?(directive, conditional_state)
|
|
938
|
+
path = directive.conditional_path || []
|
|
939
|
+
return true if path.empty?
|
|
940
|
+
|
|
941
|
+
should_execute = true
|
|
942
|
+
path_to_check = path.dup
|
|
943
|
+
if path_to_check.length >= 2
|
|
944
|
+
last_idx = path_to_check.last
|
|
945
|
+
last_state = conditional_state[last_idx]
|
|
946
|
+
if last_state && %w[elsif else].include?(last_state[:directive_type])
|
|
947
|
+
parent_if_idx = path_to_check[path_to_check.length - 2]
|
|
948
|
+
parent_if_state = conditional_state[parent_if_idx]
|
|
949
|
+
if parent_if_state && %w[if unless].include?(parent_if_state[:directive_type])
|
|
950
|
+
path_to_check.delete(parent_if_idx)
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
path_to_check.each do |cond_idx|
|
|
956
|
+
cond_state = conditional_state[cond_idx]
|
|
957
|
+
if cond_state.nil? || !cond_state[:evaluated] || !cond_state[:result]
|
|
958
|
+
should_execute = false
|
|
959
|
+
break
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
should_execute
|
|
964
|
+
end
|
|
965
|
+
|
|
942
966
|
##
|
|
943
967
|
## Find the index of the matching @if/@unless for an @elsif/@else/@end
|
|
944
968
|
##
|
data/lib/howzit/util.rb
CHANGED
|
@@ -161,6 +161,14 @@ module Howzit
|
|
|
161
161
|
status.success?
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
+
# Strip safely when external encoding rejects multibyte content (e.g. US-ASCII default).
|
|
165
|
+
def safe_strip(str)
|
|
166
|
+
s = str.to_s
|
|
167
|
+
s.strip
|
|
168
|
+
rescue Encoding::CompatibilityError
|
|
169
|
+
s.encode('UTF-8', invalid: :replace, undef: :replace).strip
|
|
170
|
+
end
|
|
171
|
+
|
|
164
172
|
# print output to terminal
|
|
165
173
|
def show(string, opts = {})
|
|
166
174
|
options = {
|
|
@@ -180,7 +188,10 @@ module Howzit
|
|
|
180
188
|
pipes = "|#{hl}" if hl
|
|
181
189
|
end
|
|
182
190
|
|
|
183
|
-
|
|
191
|
+
string = safe_strip(string)
|
|
192
|
+
|
|
193
|
+
raw_output = `echo #{Shellwords.escape(string)}#{pipes}`
|
|
194
|
+
output = safe_strip(raw_output)
|
|
184
195
|
|
|
185
196
|
if options[:paginate] && Howzit.options[:paginate]
|
|
186
197
|
page(output)
|
data/lib/howzit/version.rb
CHANGED
data/lib/howzit.rb
CHANGED
|
@@ -60,7 +60,7 @@ require 'tty/box'
|
|
|
60
60
|
# Main module for howzit
|
|
61
61
|
module Howzit
|
|
62
62
|
class << self
|
|
63
|
-
attr_accessor :arguments, :named_arguments, :cli_args, :run_log, :multi_topic_run
|
|
63
|
+
attr_accessor :arguments, :named_arguments, :cli_topic_positional_args, :cli_args, :run_log, :multi_topic_run
|
|
64
64
|
|
|
65
65
|
##
|
|
66
66
|
## Holds a Configuration object with methods and a @settings hash
|
|
@@ -122,6 +122,30 @@ describe Howzit::ConditionEvaluator do
|
|
|
122
122
|
expect(described_class.evaluate('${env} == "production"', {})).to be true
|
|
123
123
|
expect(described_class.evaluate('${var} == "other"', {})).to be false
|
|
124
124
|
end
|
|
125
|
+
|
|
126
|
+
it 'evaluates -z (empty) for named arguments' do
|
|
127
|
+
Howzit.named_arguments = { 'option' => '', other: 'x' }
|
|
128
|
+
expect(described_class.evaluate('-z option', {})).to be true
|
|
129
|
+
expect(described_class.evaluate('-z other', {})).to be false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'evaluates -n (non-empty) for named arguments' do
|
|
133
|
+
Howzit.named_arguments = { 'option' => '', other: 'x' }
|
|
134
|
+
expect(described_class.evaluate('-n option', {})).to be false
|
|
135
|
+
expect(described_class.evaluate('-n other', {})).to be true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'treats undefined named argument as empty for -z / -n' do
|
|
139
|
+
Howzit.named_arguments = {}
|
|
140
|
+
expect(described_class.evaluate('-z missing', {})).to be true
|
|
141
|
+
expect(described_class.evaluate('-n missing', {})).to be false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'supports not -z and not -n' do
|
|
145
|
+
Howzit.named_arguments = { opt: 'yes' }
|
|
146
|
+
expect(described_class.evaluate('not -z opt', {})).to be true
|
|
147
|
+
expect(described_class.evaluate('not -n opt', {})).to be false
|
|
148
|
+
end
|
|
125
149
|
end
|
|
126
150
|
|
|
127
151
|
context 'with metadata' do
|
|
@@ -317,4 +317,53 @@ describe 'Sequential Conditional Evaluation' do
|
|
|
317
317
|
expect(Howzit.named_arguments['ALL']).to eq('123')
|
|
318
318
|
end
|
|
319
319
|
end
|
|
320
|
+
|
|
321
|
+
describe '@set_var under @if/@else' do
|
|
322
|
+
let(:branch_note) do
|
|
323
|
+
<<~EONOTE
|
|
324
|
+
# Test
|
|
325
|
+
|
|
326
|
+
## Branch Topic (flag)
|
|
327
|
+
|
|
328
|
+
@if -n flag
|
|
329
|
+
@set_var(RESULT, "yes")
|
|
330
|
+
@else
|
|
331
|
+
@set_var(RESULT, "no")
|
|
332
|
+
@end
|
|
333
|
+
|
|
334
|
+
```run Echo
|
|
335
|
+
#!/bin/bash
|
|
336
|
+
echo ok
|
|
337
|
+
```
|
|
338
|
+
EONOTE
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
it 'only runs @set_var from the active branch when @if is true' do
|
|
342
|
+
File.open('builda.md', 'w') { |f| f.puts branch_note }
|
|
343
|
+
Howzit.arguments = ['present']
|
|
344
|
+
Howzit.cli_topic_positional_args = ['present']
|
|
345
|
+
Howzit.named_arguments = {}
|
|
346
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
347
|
+
topic = Howzit.buildnote('builda.md').find_topic('Branch Topic')[0]
|
|
348
|
+
allow(Howzit::Prompt).to receive(:yn).and_return(true)
|
|
349
|
+
|
|
350
|
+
topic.run
|
|
351
|
+
|
|
352
|
+
expect(Howzit.named_arguments['RESULT']).to eq('yes')
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'only runs @set_var from the active branch when @else is taken' do
|
|
356
|
+
File.open('builda.md', 'w') { |f| f.puts branch_note }
|
|
357
|
+
Howzit.arguments = []
|
|
358
|
+
Howzit.cli_topic_positional_args = []
|
|
359
|
+
Howzit.named_arguments = {}
|
|
360
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
361
|
+
topic = Howzit.buildnote('builda.md').find_topic('Branch Topic')[0]
|
|
362
|
+
allow(Howzit::Prompt).to receive(:yn).and_return(true)
|
|
363
|
+
|
|
364
|
+
topic.run
|
|
365
|
+
|
|
366
|
+
expect(Howzit.named_arguments['RESULT']).to eq('no')
|
|
367
|
+
end
|
|
368
|
+
end
|
|
320
369
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -18,11 +18,19 @@ RSpec.configure do |c|
|
|
|
18
18
|
save_buildnote
|
|
19
19
|
# Reset buildnote cache to ensure fresh instance with updated file
|
|
20
20
|
Howzit.instance_variable_set(:@buildnote, nil)
|
|
21
|
+
Howzit.arguments = []
|
|
22
|
+
Howzit.cli_topic_positional_args = nil
|
|
23
|
+
Howzit.named_arguments = {}
|
|
21
24
|
Howzit.options[:include_upstream] = false
|
|
22
25
|
Howzit.options[:stack] = false
|
|
23
26
|
Howzit.options[:default] = true
|
|
24
27
|
Howzit.options[:matching] = 'partial'
|
|
25
28
|
Howzit.options[:multiple_matches] = 'choose'
|
|
29
|
+
# Point singleton at test fixture (repo may also contain buildnotes.md, etc.)
|
|
30
|
+
Howzit.instance_variable_set(
|
|
31
|
+
:@buildnote,
|
|
32
|
+
Howzit::BuildNote.new(file: File.expand_path('builda.md'))
|
|
33
|
+
)
|
|
26
34
|
@hz = Howzit.buildnote
|
|
27
35
|
end
|
|
28
36
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
describe 'Topic title positional args vs gather_tasks' do
|
|
7
|
+
it 'uses CLI positional snapshot so a prior topic @include […] does not shift later topic params' do
|
|
8
|
+
Howzit.arguments = ['from_cli']
|
|
9
|
+
Howzit.cli_topic_positional_args = ['from_cli']
|
|
10
|
+
|
|
11
|
+
Tempfile.create(['howzit-pos', '.md']) do |f|
|
|
12
|
+
f.write(<<~MD)
|
|
13
|
+
defined: snap
|
|
14
|
+
|
|
15
|
+
# Note
|
|
16
|
+
|
|
17
|
+
## First Topic
|
|
18
|
+
|
|
19
|
+
@include(DoesNotNeedToExist[y])
|
|
20
|
+
|
|
21
|
+
## Widget Topic (only:falafel)
|
|
22
|
+
|
|
23
|
+
ok
|
|
24
|
+
MD
|
|
25
|
+
f.flush
|
|
26
|
+
|
|
27
|
+
note = Howzit::BuildNote.new(file: f.path)
|
|
28
|
+
widget = note.topics.find { |t| t.title == 'Widget Topic' }
|
|
29
|
+
expect(widget).not_to be_nil
|
|
30
|
+
expect(widget.named_args['only']).to eq('from_cli')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/spec/topic_spec.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: howzit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.41
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -331,6 +331,7 @@ files:
|
|
|
331
331
|
- spec/stack_mode_spec.rb
|
|
332
332
|
- spec/stringutils_spec.rb
|
|
333
333
|
- spec/task_spec.rb
|
|
334
|
+
- spec/topic_positional_snapshot_spec.rb
|
|
334
335
|
- spec/topic_spec.rb
|
|
335
336
|
- spec/util_spec.rb
|
|
336
337
|
- src/_README.md
|
|
@@ -354,7 +355,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
354
355
|
- !ruby/object:Gem::Version
|
|
355
356
|
version: '0'
|
|
356
357
|
requirements: []
|
|
357
|
-
rubygems_version:
|
|
358
|
+
rubygems_version: 3.6.7
|
|
358
359
|
specification_version: 4
|
|
359
360
|
summary: Provides a way to access Markdown project notes by topic with query capabilities
|
|
360
361
|
and the ability to execute the tasks it describes.
|
|
@@ -375,5 +376,6 @@ test_files:
|
|
|
375
376
|
- spec/stack_mode_spec.rb
|
|
376
377
|
- spec/stringutils_spec.rb
|
|
377
378
|
- spec/task_spec.rb
|
|
379
|
+
- spec/topic_positional_snapshot_spec.rb
|
|
378
380
|
- spec/topic_spec.rb
|
|
379
381
|
- spec/util_spec.rb
|