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