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