oktest 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  ###
4
- ### $Release: 1.0.0 $
4
+ ### $Release: 1.1.1 $
5
5
  ### $Copyright: copyright(c) 2011-2021 kuwata-lab.com all rights reserved $
6
6
  ### $License: MIT License $
7
7
  ###
data/lib/oktest.rb CHANGED
@@ -1,16 +1,18 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  ###
4
- ### $Release: 1.0.0 $
4
+ ### $Release: 1.1.1 $
5
5
  ### $Copyright: copyright(c) 2011-2021 kuwata-lab.com all rights reserved $
6
6
  ### $License: MIT License $
7
7
  ###
8
8
 
9
+ require 'set'
10
+
9
11
 
10
12
  module Oktest
11
13
 
12
14
 
13
- VERSION = '$Release: 1.0.0 $'.split()[1]
15
+ VERSION = '$Release: 1.1.1 $'.split()[1]
14
16
 
15
17
 
16
18
  class OktestError < StandardError
@@ -114,6 +116,10 @@ module Oktest
114
116
 
115
117
  def ===(expected)
116
118
  __done()
119
+ #; [!mjh4d] raises error when combination of 'not_ok()' and matcher object.
120
+ if @bool == false && @actual.is_a?(Matcher)
121
+ raise OktestError, "negative `===` is not available with matcher object."
122
+ end
117
123
  #; [!42f6a] raises assertion error when failed.
118
124
  #; [!vhvyu] is avaialbe with NOT.
119
125
  __assert(@bool == (@actual === expected)) {
@@ -264,10 +270,10 @@ module Oktest
264
270
 
265
271
  def raise!(errcls=nil, errmsg=nil, &b)
266
272
  #; [!8k6ee] compares error class by '.is_a?' instead of '=='.
267
- return raise?(errcls, errmsg, subclass: true, &b)
273
+ return raise?(errcls, errmsg, _subclass: true, &b)
268
274
  end
269
275
 
270
- def raise?(errcls=nil, errmsg=nil, subclass: false, &b)
276
+ def raise?(errcls=nil, errmsg=nil, _subclass: false, &b)
271
277
  __done()
272
278
  #; [!2rnni] 1st argument can be error message string or rexp.
273
279
  if errmsg.nil? && ! errcls.nil? && ! (errcls.is_a?(Class) && errcls <= Exception)
@@ -289,8 +295,8 @@ module Oktest
289
295
  #; [!lq6jv] compares error class with '==' operator, not '.is_a?'.
290
296
  elsif exc.class == errcls # not `exc.is_a?(errcls)`
291
297
  nil
292
- #; [!hwg0z] compares error class with '.is_a?' if 'subclass: true' specified.
293
- elsif subclass && exc.class < errcls
298
+ #; [!hwg0z] compares error class with '.is_a?' if '_subclass: true' specified.
299
+ elsif _subclass && exc.class < errcls
294
300
  nil
295
301
  #; [!4n3ed] reraises if exception is not matched to specified error class.
296
302
  else
@@ -328,8 +334,8 @@ module Oktest
328
334
  #; [!smprc] compares error class with '==' operator, not '.is_a?'.
329
335
  elsif exc.class == errcls # not `exc.is_a?(errcls)`
330
336
  __assert(false) { "#{errcls.inspect} should not be raised but got #{exc.inspect}." }
331
- #; [!34nd8] compares error class with '.is_a?' if 'subclass: true' specified.
332
- elsif subclass && exc.class < errcls
337
+ #; [!34nd8] compares error class with '.is_a?' if '_subclass: true' specified.
338
+ elsif _subclass && exc.class < errcls
333
339
  __assert(false) { "#{errcls.inspect} should not be raised but got #{exc.inspect}." }
334
340
  #; [!shxne] reraises exception if different from specified error class.
335
341
  else
@@ -548,6 +554,246 @@ module Oktest
548
554
  end
549
555
 
550
556
 
557
+ class Matcher
558
+
559
+ def initialize(actual)
560
+ @actual = actual
561
+ end
562
+
563
+ def ===(expected)
564
+ #; [!spybn] raises NotImplementedError.
565
+ raise NotImplementedError.new("#{self.class.name}#===(): not implemented yet.")
566
+ end
567
+
568
+ def ==(expected)
569
+ #; [!ymt1b] raises OktestError.
570
+ raise OktestError, "JSON(): use `===` instead of `==`."
571
+ end
572
+
573
+ def fail(errmsg)
574
+ #; [!8qpsd] raises assertion error.
575
+ raise Oktest::FAIL_EXCEPTION, errmsg
576
+ end
577
+
578
+ end
579
+
580
+
581
+ class JsonMatcher < Matcher
582
+
583
+ def ===(expected)
584
+ #; [!4uf1o] raises assertion error when JSON not matched.
585
+ _compare([], @actual, expected)
586
+ #; [!0g0u4] returns true when JSON matched.
587
+ return true
588
+ end
589
+
590
+ private
591
+
592
+ def _compare?(path, a, e)
593
+ #; [!nkvqo] returns true when nothing raised.
594
+ #; [!57m2j] returns false when assertion error raised.
595
+ _compare(path, a, e)
596
+ return true
597
+ rescue FAIL_EXCEPTION
598
+ return false
599
+ end
600
+
601
+ def _compare(path, a, e)
602
+ if a.is_a?(Hash) && e.is_a?(Hash)
603
+ _compare_hash(path, a, e)
604
+ elsif a.is_a?(Array) && e.is_a?(Array)
605
+ _compare_array(path, a, e)
606
+ elsif e.is_a?(Enumerator)
607
+ _compare_enumerator(path, a, e)
608
+ elsif e.is_a?(OR)
609
+ _compare_or(path, a, e)
610
+ elsif e.is_a?(AND)
611
+ _compare_and(path, a, e)
612
+ else
613
+ _compare_value(path, a, e)
614
+ end
615
+ end
616
+
617
+ def _compare_value(path, a, e)
618
+ #; [!1ukbv] scalar value matches to integer, string, bool, and so son.
619
+ #; [!8o55d] class object matches to instance object.
620
+ #; [!s625d] regexp object matches to string value.
621
+ #; [!aqkk0] range object matches to scalar value.
622
+ #; [!a7bfs] Set object matches to enum value.
623
+ e === a or fail <<"END"
624
+ $<JSON>#{_path(path)}: $<expected> === $<actual> : failed.
625
+ $<actual>: #{a.inspect}
626
+ $<expected>: #{e.inspect}
627
+ END
628
+ #; [!4ymj2] fails when actual value is not matched to item class of range object.
629
+ if e.is_a?(Range)
630
+ expected_class = (e.begin || e.end).class
631
+ a.is_a?(expected_class) or fail <<"END"
632
+ $<JSON>#{_path(path)}: expected #{expected_class.name} value, but got #{a.class.name} value.
633
+ $<actual>: #{a.inspect}
634
+ $<expected>: #{e.inspect}
635
+ END
636
+ end
637
+ end
638
+
639
+ def _compare_array(path, a, e)
640
+ #; [!bz74w] fails when array lengths are different.
641
+ a.length == e.length or fail <<"END"
642
+ $<JSON>#{_path(path)}: $<actual>.length == $<expected>.length : failed.
643
+ $<actual>.length: #{a.length}
644
+ $<expected>.length: #{e.length}
645
+ $<actual>: #{a.inspect}
646
+ $<expected>: #{e.inspect}
647
+ END
648
+ #; [!lh6d6] compares array items recursively.
649
+ path.push(nil)
650
+ i = -1
651
+ a.zip(e) do |a2, e2|
652
+ path[-1] = (i += 1)
653
+ _compare(path, a2, e2)
654
+ end
655
+ path.pop()
656
+ end
657
+
658
+ def _compare_hash(path, a, e)
659
+ #; [!rkv0z] compares two hashes with converting keys into string.
660
+ a2 = {}; a.each {|k, v| a2[k.to_s] = v }
661
+ e2 = {}; e.each {|k, v| e2[k.to_s] = v }
662
+ #; [!fmxyg] compares hash objects recursively.
663
+ path.push(nil)
664
+ a2.each_key do |k|
665
+ path[-1] = k
666
+ if e2.key?(k)
667
+ _compare(path, a2[k], e2[k])
668
+ #; [!jbyv6] key 'aaa?' represents optional key.
669
+ elsif e2.key?("#{k}?")
670
+ _compare(path, a2[k], e2["#{k}?"]) unless a2[k].nil?
671
+ #; [!uc4ag] key '*' matches to any key name.
672
+ elsif e2.key?("*")
673
+ _compare(path, a2[k], e2["*"])
674
+ #; [!mpbvu] fails when unexpected key exists in actual hash.
675
+ else
676
+ fail <<"END"
677
+ $<JSON>#{_path(path)}: unexpected key.
678
+ $<actual>: #{a2[k].inspect}
679
+ END
680
+ end
681
+ end
682
+ path.pop()
683
+ #; [!4oasq] fails when expected key not exist in actual hash.
684
+ (e2.keys - a2.keys).each do |k|
685
+ k =~ /\?\z/ || k == "*" or fail <<"END"
686
+ $<JSON>#{_path(path)}: key \"#{k}\" expected but not found.
687
+ $<actual>.keys: #{a2.keys.sort.inspect[1...-1]}
688
+ $<expected>.keys: #{e2.keys.sort.inspect[1...-1]}
689
+ END
690
+ end
691
+ end
692
+
693
+ def _compare_enumerator(path, a, e)
694
+ #; [!ljrmc] fails when expected is an Enumerator object and actual is not an array.
695
+ e2 = e.first
696
+ a.is_a?(Array) or fail <<"END"
697
+ $<JSON>#{_path(path)}: Array value expected but got #{a.class.name} value.
698
+ $<actual>: #{a.inspect}
699
+ $<expected>: [#{e2.inspect}].each
700
+ END
701
+ #; [!sh5cg] Enumerator object matches to repeat of rule.
702
+ path.push(nil)
703
+ a.each_with_index do |a2, i|
704
+ path[-1] = i
705
+ _compare(path, a2, e2)
706
+ end
707
+ path.pop()
708
+ end
709
+
710
+ def _compare_or(path, a, e)
711
+ #; [!eqr3b] `OR()` matches to any of arguments.
712
+ #; [!5ybfg] `OR()` can contain `AND()`.
713
+ passed = e.items.any? {|e2| _compare?(path, a, e2) }
714
+ passed or fail <<"END"
715
+ $<JSON>#{_path(path)}: $<expected> === $<actual> : failed.
716
+ $<actual>: #{a.inspect}
717
+ $<expected>: OR(#{e.items.collect(&:inspect).join(', ')})
718
+ END
719
+ end
720
+
721
+ def _compare_and(path, a, e)
722
+ #; [!4hk96] `AND()` matches to all of arguments.
723
+ #; [!scx22] `AND()` can contain `OR()`.
724
+ failed = e.items.find {|e2| ! _compare?(path, a, e2) }
725
+ ! failed or fail <<"END"
726
+ $<JSON>#{_path(path)}: $<expected> === $<actual> : failed.
727
+ $<actual>: #{a.inspect}
728
+ $<expected>: AND(#{failed.inspect})
729
+ END
730
+ end
731
+
732
+ def _path(path)
733
+ #return path.collect {|x| "/#{x}" }.join()
734
+ return path.collect {|x| "[#{x.inspect}]" }.join()
735
+ end
736
+
737
+ protected
738
+
739
+ class OR
740
+ def initialize(*items)
741
+ @items = items
742
+ end
743
+ attr_reader :items
744
+ def inspect()
745
+ #; [!2mu33] returns 'OR(...)' string.
746
+ return "OR(#{@items.collect(&:inspect).join(', ')})"
747
+ end
748
+ end
749
+
750
+ class AND
751
+ def initialize(*items)
752
+ @items = items
753
+ end
754
+ attr_reader :items
755
+ def inspect()
756
+ #; [!w43ag] returns 'AND(...)' string.
757
+ return "AND(#{@items.collect(&:inspect).join(', ')})"
758
+ end
759
+ end
760
+
761
+ class Enum < Set
762
+ alias === include? # Ruby 2.4 or older doesn't have 'Set#==='.
763
+ def inspect()
764
+ #; [!fam11] returns 'Enum(...)' string.
765
+ return "Enum(#{self.collect(&:inspect).join(', ')})"
766
+ end
767
+ end
768
+
769
+ class Length
770
+ def initialize(expected)
771
+ @expected = expected
772
+ end
773
+ def ===(actual)
774
+ #; [!03ozi] compares length of actual value with expected value.
775
+ return @expected === actual.length
776
+ end
777
+ def inspect()
778
+ #; [!nwv3e] returns 'Length(n)' string.
779
+ return "Length(#{@expected.inspect})"
780
+ end
781
+ end
782
+
783
+ class Any
784
+ def ===(actual)
785
+ #; [!mzion] returns true in any case.
786
+ true
787
+ end
788
+ def inspect()
789
+ #; [!6f0yv] returns 'Any()' string.
790
+ return "Any()"
791
+ end
792
+ end
793
+
794
+ end
795
+
796
+
551
797
  class Context
552
798
  ## * Context class is separated from ScopeNode, TopicNode, and SpecLeaf.
553
799
  ## * `topic()` and `spec()` creates subclass of Context class,
@@ -891,11 +1137,20 @@ module Oktest
891
1137
  end
892
1138
 
893
1139
  def self.scope(tag: nil, &block)
894
- #; [!vxoy1] creates new scope object.
895
- #; [!rsimc] adds scope object as child of THE_GLOBAL_SCOPE.
1140
+ #; [!kem4y] detects test script filename.
896
1141
  location = caller(1).first # caller() makes performance slower, but necessary.
897
1142
  filename = location =~ /:\d+/ ? $` : nil
898
- filename = filename.sub(/\A\.\//, '')
1143
+ #; [!6ullm] changes test script filename from absolute path to relative path.
1144
+ if filename
1145
+ pwd = Dir.pwd()
1146
+ if filename.start_with?(pwd)
1147
+ filename = filename[pwd.length..-1].sub(/\A\//, '')
1148
+ elsif filename.start_with?('./')
1149
+ filename = filename[2..-1]
1150
+ end
1151
+ end
1152
+ #; [!vxoy1] creates new scope object.
1153
+ #; [!rsimc] adds scope object as child of THE_GLOBAL_SCOPE.
899
1154
  scope = ScopeNode.new(THE_GLOBAL_SCOPE, filename, tag: tag)
900
1155
  #; [!jmc4q] raises error when nested called.
901
1156
  self.__scope(scope, &block)
@@ -1107,6 +1362,41 @@ module Oktest
1107
1362
  return Benry::Recorder.new
1108
1363
  end
1109
1364
 
1365
+ def JSON(actual)
1366
+ #; [!n0k03] creates JsonMatcher object.
1367
+ return JsonMatcher.new(actual)
1368
+ end
1369
+
1370
+ def Enum(*values)
1371
+ #; [!fbfr0] creates Enum object which is a subclass of Set.
1372
+ return JsonMatcher::Enum.new(values)
1373
+ end
1374
+
1375
+ def Bool()
1376
+ #; [!vub5j] creates a set of true and false.
1377
+ return Enum(true, false)
1378
+ end
1379
+
1380
+ def OR(*args)
1381
+ #; [!9e8im] creates `OR` object.
1382
+ return JsonMatcher::OR.new(*args)
1383
+ end
1384
+
1385
+ def AND(*args)
1386
+ #; [!38jln] creates `AND` object.
1387
+ return JsonMatcher::AND.new(*args)
1388
+ end
1389
+
1390
+ def Length(n)
1391
+ #; [!qqas3] creates Length object.
1392
+ return JsonMatcher::Length.new(n)
1393
+ end
1394
+
1395
+ def Any()
1396
+ #; [!dlo1o] creates an 'Any' object.
1397
+ return JsonMatcher::Any.new
1398
+ end
1399
+
1110
1400
  end
1111
1401
 
1112
1402
 
@@ -1206,11 +1496,25 @@ module Oktest
1206
1496
  @reporter.exit_all(self)
1207
1497
  end
1208
1498
 
1499
+ def _spec_first(node)
1500
+ a1, a2 = node.each_child.partition {|c|
1501
+ c.is_a?(SpecLeaf) || (c.is_a?(TopicNode) && c._prefix != '*')
1502
+ }
1503
+ return a1+a2
1504
+ end
1505
+ private :_spec_first
1506
+
1209
1507
  def visit_scope(scope, depth, parent)
1210
1508
  @reporter.enter_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1211
1509
  #; [!5anr7] calls before_all and after_all blocks.
1212
1510
  call_before_all_block(scope)
1213
- scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1511
+ #; [!c5cw0] run specs and case_when in advance of specs and topics when SimpleReporter.
1512
+ if @reporter.order_policy() == :spec_first
1513
+ _spec_first(scope).each {|c| c.accept_visitor(self, depth+1, scope) }
1514
+ else
1515
+ scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1516
+ end
1517
+ #
1214
1518
  call_after_all_block(scope)
1215
1519
  @reporter.exit_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1216
1520
  end
@@ -1219,7 +1523,13 @@ module Oktest
1219
1523
  @reporter.enter_topic(topic, depth)
1220
1524
  #; [!i3yfv] calls 'before_all' and 'after_all' blocks.
1221
1525
  call_before_all_block(topic)
1222
- topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1526
+ #; [!p3a5o] run specs and case_when in advance of specs and topics when SimpleReporter.
1527
+ if @reporter.order_policy() == :spec_first
1528
+ _spec_first(topic).each {|c| c.accept_visitor(self, depth+1, topic) }
1529
+ else
1530
+ topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1531
+ end
1532
+ #
1223
1533
  call_after_all_block(topic)
1224
1534
  @reporter.exit_topic(topic, depth)
1225
1535
  end
@@ -1420,6 +1730,7 @@ module Oktest
1420
1730
  def exit_spec(spec, depth, status, error, parent); end
1421
1731
  #
1422
1732
  def counts; {}; end
1733
+ def order_policy(); nil; end # :spec_first or nil
1423
1734
 
1424
1735
  end
1425
1736
 
@@ -1430,7 +1741,7 @@ module Oktest
1430
1741
  CHARS = { :PASS=>'.', :FAIL=>'f', :ERROR=>'E', :SKIP=>'s', :TODO=>'t' }
1431
1742
 
1432
1743
 
1433
- def initialize
1744
+ def initialize()
1434
1745
  @exceptions = []
1435
1746
  @counts = {}
1436
1747
  end
@@ -1573,6 +1884,10 @@ module Oktest
1573
1884
 
1574
1885
  LABELS = { :PASS=>'pass', :FAIL=>'Fail', :ERROR=>'ERROR', :SKIP=>'Skip', :TODO=>'TODO' }
1575
1886
 
1887
+ def enter_scope(scope)
1888
+ puts "## #{scope.filename}"
1889
+ end
1890
+
1576
1891
  def enter_topic(topic, depth)
1577
1892
  super
1578
1893
  puts "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}"
@@ -1606,6 +1921,64 @@ module Oktest
1606
1921
 
1607
1922
 
1608
1923
  class SimpleReporter < BaseReporter
1924
+ #; [!jxa1b] reports topics and progress.
1925
+
1926
+ def initialize()
1927
+ super
1928
+ @_nl = true
1929
+ end
1930
+
1931
+ def order_policy()
1932
+ :spec_first
1933
+ end
1934
+
1935
+ def _nl()
1936
+ (puts(); @_nl = true) unless @_nl
1937
+ end
1938
+ private :_nl
1939
+
1940
+ def _nl_off()
1941
+ @_nl = false
1942
+ end
1943
+ private :_nl_off
1944
+
1945
+ def enter_scope(scope)
1946
+ _nl()
1947
+ puts "## #{scope.filename}"
1948
+ end
1949
+
1950
+ def exit_scope(scope)
1951
+ _nl()
1952
+ print_exceptions()
1953
+ end
1954
+
1955
+ def enter_topic(topic, depth)
1956
+ super
1957
+ return if topic._prefix == '-'
1958
+ _nl()
1959
+ print "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}: "
1960
+ $stdout.flush()
1961
+ _nl_off()
1962
+ end
1963
+
1964
+ def exit_topic(topic, depth)
1965
+ super
1966
+ return if topic._prefix == '-'
1967
+ _nl()
1968
+ print_exceptions()
1969
+ end
1970
+
1971
+ def exit_spec(spec, depth, status, error, parent)
1972
+ super
1973
+ print Color.status(status, CHARS[status] || '?')
1974
+ $stdout.flush
1975
+ _nl_off()
1976
+ end
1977
+
1978
+ end
1979
+
1980
+
1981
+ class CompactReporter < BaseReporter
1609
1982
  #; [!xfd5o] reports filename.
1610
1983
 
1611
1984
  def enter_scope(scope)
@@ -1672,14 +2045,13 @@ module Oktest
1672
2045
  REPORTER_CLASSES = {
1673
2046
  'verbose' => VerboseReporter, 'v' => VerboseReporter,
1674
2047
  'simple' => SimpleReporter, 's' => SimpleReporter,
2048
+ 'compact' => CompactReporter, 'c' => CompactReporter,
1675
2049
  'plain' => PlainReporter, 'p' => PlainReporter,
1676
2050
  'quiet' => QuietReporter, 'q' => QuietReporter,
1677
2051
  }
1678
2052
 
1679
2053
 
1680
2054
  def self.run(reporter: nil, style: nil)
1681
- #; [!kfi8b] do nothing when 'Oktest.scope()' not called.
1682
- return unless THE_GLOBAL_SCOPE.has_child?
1683
2055
  #; [!6xn3t] creates reporter object according to 'style:' keyword arg.
1684
2056
  klass = (style ? REPORTER_CLASSES[style] : REPORTER_CLASS) or
1685
2057
  raise ArgumentError, "#{style.inspect}: unknown style."
@@ -2145,7 +2517,7 @@ END
2145
2517
  return 0
2146
2518
  end
2147
2519
  #; [!65vdx] prints help message if no arguments specified.
2148
- if filenames.empty?
2520
+ if filenames.empty? && !THE_GLOBAL_SCOPE.has_child?
2149
2521
  puts help_message()
2150
2522
  return 0
2151
2523
  end
@@ -2176,7 +2548,8 @@ END
2176
2548
  Config.auto_run = false
2177
2549
  #; [!18qpe] runs test scripts.
2178
2550
  #; [!0qd92] '-s verbose' or '-sv' option prints test results in verbose mode.
2179
- #; [!ef5v7] '-s simple' or '-ss' option prints test results in simple mode.
2551
+ #; [!zfdr5] '-s simple' or '-ss' option prints test results in simple mode.
2552
+ #; [!ef5v7] '-s compact' or '-sc' option prints test results in compact mode.
2180
2553
  #; [!244te] '-s plain' or '-sp' option prints test results in plain mode.
2181
2554
  #; [!ai61w] '-s quiet' or '-sq' option prints test results in quiet mode.
2182
2555
  n_errors = Oktest.run(:style=>opts.style)
@@ -2207,6 +2580,7 @@ END
2207
2580
  }
2208
2581
  parser.on('-F PATTERN') {|val|
2209
2582
  #; [!71h2x] '-F ...' option will be error.
2583
+ #; [!j01y7] if filerting by '-F' matched nothing, then prints zero result.
2210
2584
  val =~ /\A(topic|spec|tag|sid)(=|!=)/ or
2211
2585
  raise OptionParser::InvalidArgument, val
2212
2586
  opts.filter = val
@@ -2237,7 +2611,7 @@ END
2237
2611
  Usage: %{command} [<options>] [<file-or-directory>...]
2238
2612
  -h, --help : show help
2239
2613
  --version : print version
2240
- -s <STYLE> : report style (verbose/simple/plain/quiet, or v/s/p/q)
2614
+ -s <REPORT-STYLE> : verbose/simple/compact/plain/quiet, or v/s/c/p/q
2241
2615
  -F <PATTERN> : filter topic or spec with pattern (see below)
2242
2616
  --color[={on|off}] : enable/disable output coloring forcedly
2243
2617
  -C, --create : print test code skeleton