1# ========================================== 2# Unity Project - A Test Framework for C 3# Copyright (c) 2007 Mike Karlesky, Mark VanderVoord, Greg Williams 4# [Released under MIT License. Please refer to license.txt for details] 5# ========================================== 6 7# This script creates all the files with start code necessary for a new module. 8# A simple module only requires a source file, header file, and test file. 9# Triad modules require a source, header, and test file for each triad type (like model, conductor, and hardware). 10 11require 'rubygems' 12require 'fileutils' 13require 'pathname' 14 15# TEMPLATE_TST 16TEMPLATE_TST ||= '#ifdef %5$s 17 18#include "unity.h" 19 20%2$s#include "%1$s.h" 21 22void setUp(void) 23{ 24} 25 26void tearDown(void) 27{ 28} 29 30void test_%4$s_NeedToImplement(void) 31{ 32 TEST_IGNORE_MESSAGE("Need to Implement %1$s"); 33} 34 35#endif // %5$s 36'.freeze 37 38# TEMPLATE_SRC 39TEMPLATE_SRC ||= '%2$s#include "%1$s.h" 40'.freeze 41 42# TEMPLATE_INC 43TEMPLATE_INC ||= '#ifndef %3$s_H 44#define %3$s_H 45%2$s 46 47#endif // %3$s_H 48'.freeze 49 50class UnityModuleGenerator 51 ############################ 52 def initialize(options = nil) 53 @options = UnityModuleGenerator.default_options 54 case options 55 when NilClass then @options 56 when String then @options.merge!(UnityModuleGenerator.grab_config(options)) 57 when Hash then @options.merge!(options) 58 else raise 'If you specify arguments, it should be a filename or a hash of options' 59 end 60 61 # Create default file paths if none were provided 62 @options[:path_src] = "#{__dir__}/../src/" if @options[:path_src].nil? 63 @options[:path_inc] = @options[:path_src] if @options[:path_inc].nil? 64 @options[:path_tst] = "#{__dir__}/../test/" if @options[:path_tst].nil? 65 @options[:path_src] += '/' unless @options[:path_src][-1] == 47 66 @options[:path_inc] += '/' unless @options[:path_inc][-1] == 47 67 @options[:path_tst] += '/' unless @options[:path_tst][-1] == 47 68 69 # Built in patterns 70 @patterns = { 71 'src' => { 72 '' => { inc: [] } 73 }, 74 'test' => { 75 '' => { inc: [] } 76 }, 77 'dh' => { 78 'Driver' => { inc: [create_filename('%1$s', 'Hardware.h')] }, 79 'Hardware' => { inc: [] } 80 }, 81 'dih' => { 82 'Driver' => { inc: [create_filename('%1$s', 'Hardware.h'), create_filename('%1$s', 'Interrupt.h')] }, 83 'Interrupt' => { inc: [create_filename('%1$s', 'Hardware.h')] }, 84 'Hardware' => { inc: [] } 85 }, 86 'mch' => { 87 'Model' => { inc: [] }, 88 'Conductor' => { inc: [create_filename('%1$s', 'Model.h'), create_filename('%1$s', 'Hardware.h')] }, 89 'Hardware' => { inc: [] } 90 }, 91 'mvp' => { 92 'Model' => { inc: [] }, 93 'Presenter' => { inc: [create_filename('%1$s', 'Model.h'), create_filename('%1$s', 'View.h')] }, 94 'View' => { inc: [] } 95 } 96 } 97 end 98 99 ############################ 100 def self.default_options 101 { 102 pattern: 'src', 103 includes: { 104 src: [], 105 inc: [], 106 tst: [] 107 }, 108 update_svn: false, 109 boilerplates: {}, 110 test_prefix: 'Test', 111 mock_prefix: 'Mock', 112 test_define: 'TEST' 113 } 114 end 115 116 ############################ 117 def self.grab_config(config_file) 118 options = default_options 119 unless config_file.nil? || config_file.empty? 120 require_relative 'yaml_helper' 121 yaml_guts = YamlHelper.load_file(config_file) 122 options.merge!(yaml_guts[:unity] || yaml_guts[:cmock]) 123 raise "No :unity or :cmock section found in #{config_file}" unless options 124 end 125 options 126 end 127 128 ############################ 129 def files_to_operate_on(module_name, pattern = nil) 130 # strip any leading path information from the module name and save for later 131 subfolder = File.dirname(module_name) 132 module_name = File.basename(module_name) 133 134 # create triad definition 135 prefix = @options[:test_prefix] || 'Test' 136 triad = [{ ext: '.c', path: @options[:path_src], prefix: '', template: TEMPLATE_SRC, inc: :src, boilerplate: @options[:boilerplates][:src] }, 137 { ext: '.h', path: @options[:path_inc], prefix: '', template: TEMPLATE_INC, inc: :inc, boilerplate: @options[:boilerplates][:inc] }, 138 { ext: '.c', path: @options[:path_tst], prefix: prefix, template: TEMPLATE_TST, inc: :tst, boilerplate: @options[:boilerplates][:tst], test_define: @options[:test_define] }] 139 140 # prepare the pattern for use 141 pattern = (pattern || @options[:pattern] || 'src').downcase 142 patterns = @patterns[pattern] 143 raise "ERROR: The design pattern '#{pattern}' specified isn't one that I recognize!" if patterns.nil? 144 145 # single file patterns (currently just 'test') can reject the other parts of the triad 146 triad.select! { |v| v[:inc] == :tst } if pattern == 'test' 147 148 # Assemble the path/names of the files we need to work with. 149 files = [] 150 triad.each do |cfg| 151 patterns.each_pair do |pattern_file, pattern_traits| 152 submodule_name = create_filename(module_name, pattern_file) 153 filename = cfg[:prefix] + submodule_name + cfg[:ext] 154 files << { 155 path: (Pathname.new("#{cfg[:path]}#{subfolder}") + filename).cleanpath, 156 name: submodule_name, 157 template: cfg[:template], 158 test_define: cfg[:test_define], 159 boilerplate: cfg[:boilerplate], 160 includes: case (cfg[:inc]) 161 when :src then (@options[:includes][:src] || []) | (pattern_traits[:inc].map { |f| format(f, module_name) }) 162 when :inc then (@options[:includes][:inc] || []) 163 when :tst then (@options[:includes][:tst] || []) | (pattern_traits[:inc].map { |f| format("#{@options[:mock_prefix]}#{f}", module_name) }) 164 end 165 } 166 end 167 end 168 169 files 170 end 171 172 ############################ 173 def neutralize_filename(name, start_cap: true) 174 return name if name.empty? 175 176 name = name.split(/(?:\s+|_|(?=[A-Z][a-z]))|(?<=[a-z])(?=[A-Z])/).map(&:capitalize).join('_') 177 name = name[0].downcase + name[1..] unless start_cap 178 name 179 end 180 181 ############################ 182 def create_filename(part1, part2 = '') 183 name = part2.empty? ? part1 : "#{part1}_#{part2}" 184 case (@options[:naming]) 185 when 'bumpy' then neutralize_filename(name, start_cap: false).delete('_') 186 when 'camel' then neutralize_filename(name).delete('_') 187 when 'snake' then neutralize_filename(name).downcase 188 when 'caps' then neutralize_filename(name).upcase 189 else name 190 end 191 end 192 193 ############################ 194 def generate(module_name, pattern = nil) 195 files = files_to_operate_on(module_name, pattern) 196 197 # Abort if all of the module files already exist 198 all_files_exist = true 199 files.each do |file| 200 all_files_exist = false unless File.exist?(file[:path]) 201 end 202 raise "ERROR: File #{files[0][:name]} already exists. Exiting." if all_files_exist 203 204 # Create Source Modules 205 files.each_with_index do |file, _i| 206 # If this file already exists, don't overwrite it. 207 if File.exist?(file[:path]) 208 puts "File #{file[:path]} already exists!" 209 next 210 end 211 # Create the path first if necessary. 212 FileUtils.mkdir_p(File.dirname(file[:path]), verbose: false) 213 File.open(file[:path], 'w') do |f| 214 f.write("#{file[:boilerplate]}\n" % [file[:name]]) unless file[:boilerplate].nil? 215 f.write(file[:template] % [file[:name], 216 file[:includes].map { |ff| "#include \"#{ff}\"\n" }.join, 217 file[:name].upcase.tr('-', '_'), 218 file[:name].tr('-', '_'), 219 file[:test_define]]) 220 end 221 if @options[:update_svn] 222 `svn add \"#{file[:path]}\"` 223 if $!.exitstatus.zero? 224 puts "File #{file[:path]} created and added to source control" 225 else 226 puts "File #{file[:path]} created but FAILED adding to source control!" 227 end 228 else 229 puts "File #{file[:path]} created" 230 end 231 end 232 puts 'Generate Complete' 233 end 234 235 ############################ 236 def destroy(module_name, pattern = nil) 237 files_to_operate_on(module_name, pattern).each do |filespec| 238 file = filespec[:path] 239 if File.exist?(file) 240 if @options[:update_svn] 241 `svn delete \"#{file}\" --force` 242 puts "File #{file} deleted and removed from source control" 243 else 244 FileUtils.remove(file) 245 puts "File #{file} deleted" 246 end 247 else 248 puts "File #{file} does not exist so cannot be removed." 249 end 250 end 251 puts 'Destroy Complete' 252 end 253end 254 255############################ 256# Handle As Command Line If Called That Way 257if $0 == __FILE__ 258 destroy = false 259 options = {} 260 module_name = nil 261 262 # Parse the command line parameters. 263 ARGV.each do |arg| 264 case arg 265 when /^-d/ then destroy = true 266 when /^-u/ then options[:update_svn] = true 267 when /^-p"?(\w+)"?/ then options[:pattern] = Regexp.last_match(1) 268 when /^-s"?(.+)"?/ then options[:path_src] = Regexp.last_match(1) 269 when /^-i"?(.+)"?/ then options[:path_inc] = Regexp.last_match(1) 270 when /^-t"?(.+)"?/ then options[:path_tst] = Regexp.last_match(1) 271 when /^-n"?(.+)"?/ then options[:naming] = Regexp.last_match(1) 272 when /^-y"?(.+)"?/ then options = UnityModuleGenerator.grab_config(Regexp.last_match(1)) 273 when /^(\w+)/ 274 raise "ERROR: You can't have more than one Module name specified!" unless module_name.nil? 275 276 module_name = arg 277 when /^-(h|-help)/ 278 ARGV = [].freeze 279 else 280 raise "ERROR: Unknown option specified '#{arg}'" 281 end 282 end 283 284 unless ARGV[0] 285 puts ["\nGENERATE MODULE\n-------- ------", 286 "\nUsage: ruby generate_module [options] module_name", 287 " -i\"include\" sets the path to output headers to 'include' (DEFAULT ../src)", 288 " -s\"../src\" sets the path to output source to '../src' (DEFAULT ../src)", 289 " -t\"C:/test\" sets the path to output source to 'C:/test' (DEFAULT ../test)", 290 ' -p"MCH" sets the output pattern to MCH.', 291 ' dh - driver hardware.', 292 ' dih - driver interrupt hardware.', 293 ' mch - model conductor hardware.', 294 ' mvp - model view presenter.', 295 ' src - just a source module, header and test. (DEFAULT)', 296 ' test - just a test file.', 297 ' -d destroy module instead of creating it.', 298 ' -n"camel" sets the file naming convention.', 299 ' bumpy - BumpyCaseFilenames.', 300 ' camel - camelCaseFilenames.', 301 ' snake - snake_case_filenames.', 302 ' caps - CAPS_CASE_FILENAMES.', 303 ' -u update subversion too (requires subversion command line)', 304 ' -y"my.yml" selects a different yaml config file for module generation', 305 ''].join("\n") 306 exit 307 end 308 309 raise 'ERROR: You must have a Module name specified! (use option -h for help)' if module_name.nil? 310 311 if destroy 312 UnityModuleGenerator.new(options).destroy(module_name) 313 else 314 UnityModuleGenerator.new(options).generate(module_name) 315 end 316 317end 318