1# encoding: utf-8
2require 'set'
3require 'yaml'
4require 'command_line_reporter'
5
6require_relative 'directory'
7
8class SassSpec::Annotate::CLI
9  include CommandLineReporter
10
11  def self.assert_legal_version(version)
12    if version && !SassSpec::LANGUAGE_VERSIONS.include?(version)
13      warn "Version #{version} is not valid. " +
14           "Did you mean one of: #{SassSpec::LANGUAGE_VERSIONS.join(', ')}"
15      return false
16    end
17    true
18  end
19
20  def self.assert_not_file!(string, expected)
21    if File.exist?(string) || string.include?(File::SEPARATOR)
22      warn "Expected #{expected} but got a file path. Did you forget the argument?"
23      return false
24    end
25    true
26  end
27
28  def self.parse(args)
29    runner_options = {
30    }
31    options = {
32    }
33    parser = OptionParser.new do |opts|
34      opts.banner = <<BANNER
35Usage: ./sass-spec.rb annotate [options] PATH [PATH...]
36
37This sub command helps you annotate spec tests.
38
39BANNER
40
41      opts.on("--start-version VERSION",
42              "Set the Sass language first version for which the test(s) are valid.",
43              "Pass a version of 'unset' to remove the start version.") do |version|
44        version = nil if version =~ /unset/i
45        return unless assert_legal_version(version)
46        options[:start_version] = version
47      end
48
49      opts.on("--end-version VERSION",
50              "Set the Sass language first version for which the test(s) are valid.",
51              "Pass a version of 'unset' to remove the end version.") do |version|
52        version = nil if version =~ /unset/i
53        return unless assert_legal_version(version)
54        options[:end_version] = version
55      end
56
57      opts.on("--precision INTEGER",
58              "Set the numeric output precision for the specified tests.",
59              "Pass a precision of 'unset' to remove the precision.") do |precision|
60        if precision =~ /unset/i
61          precision = nil
62        elsif precision =~ /^\d+$/
63          precision = precision.to_i
64        else
65          warn "Precision must be set to a positive integer (or to 'unset')\n\n"
66          warn opts.help()
67          return nil
68        end
69        options[:precision] = precision
70      end
71
72      opts.on("--pending IMPLEMENTATION",
73              "Mark implementation as not having implemented the tests.") do |impl|
74        return unless assert_not_file!(impl, "implementation for --pending")
75        (options[:add_todo] ||= Set.new) << impl
76      end
77
78      opts.on("--activate IMPLEMENTATION",
79              "Mark implementation as having implemented the tests.") do |impl|
80        return unless assert_not_file!(impl, "implementation for --activate")
81        (options[:remove_todo] ||= Set.new) << impl
82      end
83
84      opts.on("--pending-warning IMPLEMENTATION",
85              "Mark implementation as not having implemented the warnings issued by the tests.") do |impl|
86        return unless assert_not_file!(impl, "implementation for --pending")
87        (options[:add_warning_todo] ||= Set.new) << impl
88      end
89
90      opts.on("--activate-warning IMPLEMENTATION",
91              "Mark implementation as having implemented the warnings issued by the tests.") do |impl|
92        return unless assert_not_file!(impl, "implementation for --activate")
93        (options[:remove_warning_todo] ||= Set.new) << impl
94      end
95
96      opts.on("--ignore-for IMPLEMENTATION",
97              "Flag test so that it won't run against the specified implementation.") do |impl|
98        return unless assert_not_file!(impl, "implementation for --ignore-for")
99        (options[:add_ignore_for] ||= Set.new) << impl
100      end
101
102      opts.on("--unignore-for IMPLEMENTATION",
103              "Remove the ignore flag so that the test will run against the specified implementation.") do |impl|
104        return unless assert_not_file!(impl, "implementation for --unignore-for")
105        (options[:remove_ignore_for] ||= Set.new) << impl
106      end
107
108      opts.on("--ignore-warning-for IMPLEMENTATION",
109              "Flag test so that it won't check warnings with the specified implementation.") do |impl|
110        return unless assert_not_file!(impl, "implementation for --ignore-warning-for")
111        (options[:add_ignore_warning_for] ||= Set.new) << impl
112      end
113
114      opts.on("--unignore-warning-for IMPLEMENTATION",
115              "Remove the ignore flag so that warnings are checked with the specified implementation.") do |impl|
116        return unless assert_not_file!(impl, "implementation for --unignore-warning-for")
117        (options[:remove_ignore_warning_for] ||= Set.new) << impl
118      end
119
120      opts.on("--report", "Generate a report after running.") do |impl|
121        runner_options[:report] = true
122      end
123
124      opts.on("-h", "--help", "Print this help message.") do |impl|
125        puts opts.help()
126        return nil
127      end
128    end
129    parser.parse!(args)
130    if args.empty?
131      warn parser.help
132      return nil
133    end
134    args.each do |path|
135      unless File.exists?(path)
136        warn "Error: #{path} does not exist."
137        return nil
138      end
139    end
140    new(options, runner_options, args)
141  end
142
143  def initialize(options, runner_options, paths)
144    @runner_options = runner_options
145    @options = options
146    @paths = paths
147  end
148
149  def annotate
150    @paths.each {|path| annotate_path(path)}
151    if @runner_options[:report]
152      require 'terminfo'
153      @paths.each {|path| report_path(path)}
154    end
155    return true
156  end
157
158  # If you change this, also change TestCaseMetadata.merge_options
159  def annotate_path(path)
160    report(message: "#{path}:", complete: "") do
161      options_file = path.end_with?("options.yml") ? path : File.join(path, "options.yml")
162      if File.exists?(options_file)
163        current_options = YAML.load_file(options_file)
164      else
165        current_options = {}
166      end
167      @options.each do |(key, value)|
168        if key =~ /add_(.*)/
169          key = $1.to_sym
170          current_options[key] ||= []
171          value.each do |v|
172            current_options[key] << v
173            log("* adding #{v} to #{key} list")
174          end
175          current_options[key].uniq!
176        elsif key =~ /remove_(.*)/
177          key = $1.to_sym
178          current_options[key] ||= []
179          value.each do |v|
180            current_options[key].delete(v)
181            log("* removing #{v} from #{key} list")
182          end
183          current_options.delete(key) if current_options[key].empty?
184        elsif value.nil?
185          current_options.delete(key)
186          log("* unsetting #{key}") {}
187        else
188          current_options[key] = value
189          log("* setting #{key} to #{value}") {}
190        end
191      end
192
193      if current_options.empty?
194        File.delete(options_file) if File.exists?(options_file)
195      else
196        File.write(options_file, current_options.to_yaml)
197      end
198    end
199  end
200
201  def report_path(path)
202    test_case_dirs = Dir.glob(File.join(path, "**/input.scss")).map {|p| File.dirname(p) }.uniq.sort
203    metadatas = test_case_dirs.map {|d| SassSpec::TestCaseMetadata.new(SassSpec::Directory.new(d))}
204    TermInfo.screen_size
205    max_length = [
206      metadatas.map {|m| m.name.length}.max,
207      TermInfo.screen_size.last - SassSpec::LANGUAGE_VERSIONS.length * 6 - 4
208    ].min
209    table(border: true, encoding: :ascii, width: max_length + SassSpec::LANGUAGE_VERSIONS.length * 3) do
210      row(header: true) do
211        column("Test Case", width: max_length)
212        SassSpec::LANGUAGE_VERSIONS.each do |version|
213          column(version, width: 3, align: "center")
214        end
215      end
216      metadatas.each do |md|
217        row do
218          report_test_case(md)
219        end
220      end
221    end
222  end
223
224  def report_test_case(metadata)
225    column(metadata.name)
226    SassSpec::LANGUAGE_VERSIONS.each do |version|
227      v = Gem::Version.new(version)
228      if metadata.valid_for_version?(v)
229        column("✓")
230      else
231        column("")
232      end
233    end
234  end
235
236  def log(message, &block)
237    block = Proc.new {} unless block
238    report(message: message, type: 'inline', complete: "done", &block)
239  end
240end
241