doing 2.1.21 → 2.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -16
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +51 -13
  6. data/Gemfile.lock +1 -1
  7. data/README.md +1 -1
  8. data/Rakefile +6 -3
  9. data/bin/doing +254 -103
  10. data/docs/doc/Array.html +74 -34
  11. data/docs/doc/BooleanTermParser/Clause.html +5 -5
  12. data/docs/doc/BooleanTermParser/Operator.html +4 -4
  13. data/docs/doc/BooleanTermParser/Query.html +8 -8
  14. data/docs/doc/BooleanTermParser/QueryParser.html +2 -2
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +2 -2
  16. data/docs/doc/BooleanTermParser.html +1 -1
  17. data/docs/doc/Doing/Color.html +4 -4
  18. data/docs/doc/Doing/Completion.html +2 -2
  19. data/docs/doc/Doing/Configuration.html +16 -18
  20. data/docs/doc/Doing/Errors/DoingNoTraceError.html +2 -2
  21. data/docs/doc/Doing/Errors/DoingRuntimeError.html +2 -2
  22. data/docs/doc/Doing/Errors/DoingStandardError.html +2 -2
  23. data/docs/doc/Doing/Errors/EmptyInput.html +2 -2
  24. data/docs/doc/Doing/Errors/NoResults.html +2 -2
  25. data/docs/doc/Doing/Errors/PluginException.html +3 -3
  26. data/docs/doc/Doing/Errors/UserCancelled.html +2 -2
  27. data/docs/doc/Doing/Errors/WrongCommand.html +2 -2
  28. data/docs/doc/Doing/Errors.html +1 -1
  29. data/docs/doc/Doing/Hooks.html +6 -6
  30. data/docs/doc/Doing/Item.html +50 -16
  31. data/docs/doc/Doing/Items.html +10 -10
  32. data/docs/doc/Doing/LogAdapter.html +24 -24
  33. data/docs/doc/Doing/Note.html +7 -7
  34. data/docs/doc/Doing/Pager.html +4 -4
  35. data/docs/doc/Doing/Plugins.html +7 -7
  36. data/docs/doc/Doing/Prompt.html +16 -16
  37. data/docs/doc/Doing/Section.html +6 -6
  38. data/docs/doc/Doing/TemplateString.html +8 -8
  39. data/docs/doc/Doing/Types.html +206 -0
  40. data/docs/doc/Doing/Util/Backup.html +10 -10
  41. data/docs/doc/Doing/Util.html +16 -19
  42. data/docs/doc/Doing/WWID.html +65 -53
  43. data/docs/doc/Doing.html +4 -4
  44. data/docs/doc/GLI/Commands/Help.html +185 -0
  45. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +17 -17
  46. data/docs/doc/GLI/Commands.html +5 -3
  47. data/docs/doc/GLI.html +4 -2
  48. data/docs/doc/Hash.html +119 -21
  49. data/docs/doc/Numeric.html +5 -5
  50. data/docs/doc/PhraseParser/Operator.html +4 -4
  51. data/docs/doc/PhraseParser/PhraseClause.html +5 -5
  52. data/docs/doc/PhraseParser/Query.html +10 -10
  53. data/docs/doc/PhraseParser/QueryParser.html +2 -2
  54. data/docs/doc/PhraseParser/QueryTransformer.html +2 -2
  55. data/docs/doc/PhraseParser/TermClause.html +5 -5
  56. data/docs/doc/PhraseParser.html +1 -1
  57. data/docs/doc/Status.html +7 -7
  58. data/docs/doc/String.html +206 -51
  59. data/docs/doc/Symbol.html +8 -8
  60. data/docs/doc/Time.html +6 -6
  61. data/docs/doc/_index.html +51 -14
  62. data/docs/doc/class_list.html +1 -1
  63. data/docs/doc/file.README.html +2 -2
  64. data/docs/doc/index.html +2 -2
  65. data/docs/doc/method_list.html +348 -252
  66. data/docs/doc/top-level-namespace.html +2 -93
  67. data/docs/index.md +1 -1
  68. data/doing.rdoc +177 -20
  69. data/example_plugin.rb +2 -2
  70. data/lib/completion/_doing.zsh +24 -24
  71. data/lib/completion/doing.bash +31 -20
  72. data/lib/completion/doing.fish +32 -10
  73. data/lib/doing/array.rb +5 -4
  74. data/lib/doing/array_chronify.rb +4 -3
  75. data/lib/doing/changelog/change.rb +115 -0
  76. data/lib/doing/changelog/changes.rb +73 -0
  77. data/lib/doing/changelog/entry.rb +21 -0
  78. data/lib/doing/changelog/version.rb +97 -0
  79. data/lib/doing/changelog.rb +6 -0
  80. data/lib/doing/completion/fish_completion.rb +2 -1
  81. data/lib/doing/configuration.rb +20 -14
  82. data/lib/doing/good.rb +64 -0
  83. data/lib/doing/hash.rb +28 -5
  84. data/lib/doing/help_monkey_patch.rb +31 -0
  85. data/lib/doing/hooks.rb +8 -4
  86. data/lib/doing/item.rb +24 -35
  87. data/lib/doing/log_adapter.rb +1 -1
  88. data/lib/doing/pager.rb +1 -1
  89. data/lib/doing/plugins/export/template_export.rb +9 -3
  90. data/lib/doing/plugins/import/calendar_import.rb +1 -1
  91. data/lib/doing/plugins/import/doing_import.rb +1 -1
  92. data/lib/doing/plugins/import/timing_import.rb +1 -1
  93. data/lib/doing/prompt.rb +4 -2
  94. data/lib/doing/string.rb +30 -12
  95. data/lib/doing/string_chronify.rb +1 -1
  96. data/lib/doing/template_string.rb +9 -2
  97. data/lib/doing/types.rb +23 -16
  98. data/lib/doing/util.rb +12 -11
  99. data/lib/doing/version.rb +1 -1
  100. data/lib/doing/wwid.rb +65 -46
  101. data/lib/doing.rb +3 -0
  102. data/lib/helpers/threaded_tests.rb +106 -99
  103. data/lib/helpers/threaded_tests_string.rb +50 -0
  104. metadata +12 -2
data/lib/doing/wwid.rb CHANGED
@@ -338,6 +338,7 @@ module Doing
338
338
  ## @option opt :note [Array] item note (will be converted if value is String)
339
339
  ## @option opt :back [Date] backdate
340
340
  ## @option opt :timed [Boolean] new item is timed entry, marks previous entry as @done
341
+ ## @option opt :done [Date] If set, adds a @done tag to new entry
341
342
  ##
342
343
  def add_item(title, section = nil, opt)
343
344
  opt ||= {}
@@ -360,9 +361,18 @@ module Doing
360
361
 
361
362
  title.compress!
362
363
  entry = Item.new(opt[:back], title.strip, section)
364
+
365
+ if opt[:done] && entry.should_finish?
366
+ if entry.should_time?
367
+ entry.tag('done', value: opt[:done])
368
+ else
369
+ entry.tag('done')
370
+ end
371
+ end
372
+
363
373
  entry.note = note
364
374
 
365
- items = @content.dup
375
+ items = @content.clone
366
376
  if opt[:timed]
367
377
  items.reverse!
368
378
  items.each_with_index do |i, x|
@@ -380,7 +390,8 @@ module Doing
380
390
  # logger.count(:added, level: :debug)
381
391
  logger.info('New entry:', %(added "#{entry.date.relative_date}: #{entry.title}" to #{section}))
382
392
 
383
- Hooks.trigger :post_entry_added, self, entry.dup
393
+ Hooks.trigger :post_entry_added, self, entry
394
+ entry
384
395
  end
385
396
 
386
397
  ##
@@ -474,6 +485,7 @@ module Doing
474
485
  #
475
486
  def repeat_item(item, opt)
476
487
  opt ||= {}
488
+ old_item = item.clone
477
489
  if item.should_finish?
478
490
  if item.should_time?
479
491
  finish_date = verify_duration(item.date, Time.now, title: item.title)
@@ -481,7 +493,7 @@ module Doing
481
493
  else
482
494
  item.title.tag!('done')
483
495
  end
484
- Hooks.trigger :post_entry_updated, self, item
496
+ Hooks.trigger :post_entry_updated, self, item, old_item
485
497
  end
486
498
 
487
499
  # Remove @done tag
@@ -651,20 +663,14 @@ module Doing
651
663
 
652
664
  if items.nil? || items.empty?
653
665
  section = opt[:section] ? guess_section(opt[:section]) : 'All'
654
- items = section =~ /^all$/i ? @content.dup : @content.in_section(section)
666
+ items = section =~ /^all$/i ? @content.clone : @content.in_section(section)
655
667
  end
656
668
 
657
669
  opt[:time_filter] = [nil, nil]
658
670
  if opt[:from] && !opt[:date_filter]
659
671
  if opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx
660
- time_start, time_end = opt[:from]
661
- elsif opt[:from].is_a?(Time)
662
- start, finish = opt[:from]
663
- end
664
-
665
- if time_start
666
- opt[:time_filter] = [time_start, time_end]
667
- else
672
+ opt[:time_filter] = opt[:from]
673
+ elsif opt[:from][0].is_a?(Time)
668
674
  opt[:date_filter] = opt[:from]
669
675
  end
670
676
  end
@@ -853,7 +859,7 @@ module Doing
853
859
  note.map!(&:strip)
854
860
  note.delete_if(&:ignore?)
855
861
  item = items[i]
856
- old_item = item.dup
862
+ old_item = item.clone
857
863
  item.date = date || items[i].date
858
864
  item.title = title
859
865
  item.note = note
@@ -861,7 +867,7 @@ module Doing
861
867
  Doing.logger.count(:skipped, level: :debug)
862
868
  else
863
869
  Doing.logger.count(:updated)
864
- Hooks.trigger :post_entry_updated, self, item
870
+ Hooks.trigger :post_entry_updated, self, item, old_item
865
871
  end
866
872
  end
867
873
  end
@@ -1051,9 +1057,10 @@ module Doing
1051
1057
  else
1052
1058
  opt[:resume]
1053
1059
  end
1060
+ old_item = item.clone
1054
1061
  new_entry = reset_item(item, date: date, resume: res)
1055
1062
  @content.update_item(item, new_entry)
1056
- Hooks.trigger :post_entry_updated, self, new_entry
1063
+ Hooks.trigger :post_entry_updated, self, new_entry, old_item
1057
1064
  end
1058
1065
  write(@doing_file)
1059
1066
 
@@ -1068,8 +1075,9 @@ module Doing
1068
1075
  if opt[:flag]
1069
1076
  tag = @config['marker_tag'] || 'flagged'
1070
1077
  items.map! do |i|
1078
+ old_item = i.clone
1071
1079
  i.tag(tag, date: false, remove: opt[:remove], single: single)
1072
- Hooks.trigger :post_entry_updated, self, i
1080
+ Hooks.trigger :post_entry_updated, self, i, old_item
1073
1081
  end
1074
1082
  end
1075
1083
 
@@ -1077,9 +1085,10 @@ module Doing
1077
1085
  tag = 'done'
1078
1086
  items.map! do |i|
1079
1087
  if i.should_finish?
1088
+ old_item = i.clone
1080
1089
  should_date = !opt[:cancel] && i.should_time?
1081
1090
  i.tag(tag, date: should_date, remove: opt[:remove], single: single)
1082
- Hooks.trigger :post_entry_updated, self, i
1091
+ Hooks.trigger :post_entry_updated, self, i, old_item
1083
1092
  end
1084
1093
  end
1085
1094
  end
@@ -1093,8 +1102,9 @@ module Doing
1093
1102
  else
1094
1103
  logger.count(:added_tags)
1095
1104
  logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
1105
+ old_item = i.clone
1096
1106
  i.title = new_title
1097
- Hooks.trigger :post_entry_updated, self, i
1107
+ Hooks.trigger :post_entry_updated, self, i, old_item
1098
1108
  end
1099
1109
  end
1100
1110
  end
@@ -1102,17 +1112,19 @@ module Doing
1102
1112
  if opt[:tag]
1103
1113
  tag = opt[:tag]
1104
1114
  items.map! do |i|
1115
+ old_item = i.clone
1105
1116
  i.tag(tag, date: false, remove: opt[:remove], single: single)
1106
1117
  i.expand_date_tags(@config['date_tags'])
1107
- Hooks.trigger :post_entry_updated, self, i
1118
+ Hooks.trigger :post_entry_updated, self, i, old_item
1108
1119
  end
1109
1120
  end
1110
1121
 
1111
1122
  if opt[:archive] || opt[:move]
1112
1123
  section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
1113
1124
  items.map! do |i|
1125
+ old_item = i.clone
1114
1126
  i.move_to(section, label: true)
1115
- Hooks.trigger :post_entry_updated, self, i
1127
+ Hooks.trigger :post_entry_updated, self, i, old_item
1116
1128
  end
1117
1129
  end
1118
1130
 
@@ -1234,6 +1246,7 @@ module Doing
1234
1246
  end
1235
1247
 
1236
1248
  items.each do |item|
1249
+ old_item = item.clone
1237
1250
  added = []
1238
1251
  removed = []
1239
1252
 
@@ -1271,7 +1284,7 @@ module Doing
1271
1284
 
1272
1285
  tag = tag.strip
1273
1286
 
1274
- if tag =~ /^done$/
1287
+ if tag =~ /^done$/ && opt[:date] && item.should_time?
1275
1288
  max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
1276
1289
  max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
1277
1290
  elapsed = done_date - item.date
@@ -1328,7 +1341,7 @@ module Doing
1328
1341
  end
1329
1342
 
1330
1343
  item.expand_date_tags(@config['date_tags'])
1331
- Hooks.trigger :post_entry_updated, self, item
1344
+ Hooks.trigger :post_entry_updated, self, item, old_item
1332
1345
  end
1333
1346
 
1334
1347
  write(@doing_file)
@@ -1367,6 +1380,7 @@ module Doing
1367
1380
  return
1368
1381
  end
1369
1382
 
1383
+ old_item = item.clone
1370
1384
  content = ["#{item.date.strftime('%F %R')} | #{item.title.dup}"]
1371
1385
  content << item.note.strip_lines.join("\n") unless item.note.empty?
1372
1386
  new_item = fork_editor(content.join("\n"))
@@ -1382,7 +1396,7 @@ module Doing
1382
1396
  item.title = title
1383
1397
  item.note.add(note, replace: true)
1384
1398
  logger.info('Edited:', item.title)
1385
- Hooks.trigger :post_entry_updated, self, item.dup
1399
+ Hooks.trigger :post_entry_updated, self, item, old_item
1386
1400
 
1387
1401
  write(@doing_file)
1388
1402
  end
@@ -1419,6 +1433,7 @@ module Doing
1419
1433
  found_items = 0
1420
1434
 
1421
1435
  @content.each_with_index do |item, i|
1436
+ old_item = i.clone
1422
1437
  next unless item.section == opt[:section] || opt[:section] =~ /all/i
1423
1438
 
1424
1439
  next unless item.title =~ /@#{tag}/
@@ -1437,7 +1452,7 @@ module Doing
1437
1452
  logger.count(:completed)
1438
1453
  logger.info('Completed:', item.title)
1439
1454
  end
1440
- Hooks.trigger :post_entry_updated, self, item
1455
+ Hooks.trigger :post_entry_updated, self, item, old_item
1441
1456
  end
1442
1457
 
1443
1458
 
@@ -1498,7 +1513,7 @@ module Doing
1498
1513
 
1499
1514
  unless ((!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s)) || (opt[:before] && item.date >= cutoff))
1500
1515
  new_item = @content.delete(item)
1501
- Hooks.trigger :post_entry_removed, self, item.dup
1516
+ Hooks.trigger :post_entry_removed, self, item.clone
1502
1517
  raise DoingRuntimeError, "Error deleting item: #{item}" if new_item.nil?
1503
1518
 
1504
1519
  new_content.add_section(new_item.section, log: false)
@@ -1622,6 +1637,7 @@ module Doing
1622
1637
  'duration' => @config['duration'],
1623
1638
  'interval_format' => @config['interval_format']
1624
1639
  }, { extend_existing_arrays: true, sort_merged_arrays: true })
1640
+
1625
1641
  opt[:duration] ||= cfg['duration'] || false
1626
1642
  opt[:interval_format] ||= cfg['interval_format'] || 'text'
1627
1643
  opt[:count] ||= 0
@@ -1724,7 +1740,7 @@ module Doing
1724
1740
  opt[:totals] ||= false
1725
1741
  opt[:sort_tags] ||= false
1726
1742
 
1727
- cfg = @config['templates']['today'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1743
+ cfg = @config['templates'][opt[:config_template]].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1728
1744
  'wrap_width' => @config['wrap_width'] || 0,
1729
1745
  'date_format' => @config['default_date_format'],
1730
1746
  'order' => @config['order'] || 'asc',
@@ -1733,6 +1749,8 @@ module Doing
1733
1749
  'interval_format' => @config['interval_format']
1734
1750
  }, { extend_existing_arrays: true, sort_merged_arrays: true })
1735
1751
 
1752
+ template = opt[:template] || cfg['template']
1753
+
1736
1754
  opt[:duration] ||= cfg['duration'] || false
1737
1755
  opt[:interval_format] ||= cfg['interval_format'] || 'text'
1738
1756
 
@@ -1748,13 +1766,13 @@ module Doing
1748
1766
  output: output,
1749
1767
  section: opt[:section],
1750
1768
  sort_tags: opt[:sort_tags],
1751
- template: cfg['template'],
1769
+ template: template,
1752
1770
  times: times,
1753
1771
  today: true,
1754
1772
  totals: opt[:totals],
1755
1773
  wrap_width: cfg['wrap_width'],
1756
1774
  tags_color: cfg['tags_color'],
1757
- config_template: 'today'
1775
+ config_template: opt[:config_template]
1758
1776
  }
1759
1777
  list_section(options)
1760
1778
  end
@@ -1786,7 +1804,8 @@ module Doing
1786
1804
  totals: opt[:totals],
1787
1805
  duration: opt[:duration],
1788
1806
  sort_tags: opt[:sort_tags],
1789
- config_template: 'default'
1807
+ template: opt[:template],
1808
+ config_template: opt[:config_template]
1790
1809
  })
1791
1810
  end
1792
1811
 
@@ -1821,7 +1840,8 @@ module Doing
1821
1840
  times: times,
1822
1841
  totals: opt[:totals],
1823
1842
  yesterday: true,
1824
- config_template: 'today'
1843
+ config_template: opt[:config_template] || 'today',
1844
+ template: opt[:template]
1825
1845
  }
1826
1846
 
1827
1847
  list_section(options)
@@ -1840,7 +1860,7 @@ module Doing
1840
1860
  opt[:totals] ||= false
1841
1861
  opt[:sort_tags] ||= false
1842
1862
 
1843
- cfg = @config['templates']['recent'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1863
+ cfg = @config['templates'][opt[:config_template]].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1844
1864
  'wrap_width' => @config['wrap_width'] || 0,
1845
1865
  'date_format' => @config['default_date_format'],
1846
1866
  'order' => @config['order'] || 'asc',
@@ -1858,10 +1878,9 @@ module Doing
1858
1878
  opt[:wrap_width] = cfg['wrap_width']
1859
1879
  opt[:count] = count
1860
1880
  opt[:format] = cfg['date_format']
1861
- opt[:template] = cfg['template']
1881
+ opt[:template] = opt[:template] || cfg['template']
1862
1882
  opt[:order] = 'asc'
1863
1883
  opt[:times] = times
1864
- opt[:config_template] = 'recent'
1865
1884
 
1866
1885
  list_section(opt)
1867
1886
  end
@@ -1874,7 +1893,7 @@ module Doing
1874
1893
  ##
1875
1894
  def last(times: true, section: nil, options: {})
1876
1895
  section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
1877
- cfg = @config['templates']['last'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1896
+ cfg = @config['templates'][options[:config_template]].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1878
1897
  'wrap_width' => @config['wrap_width'] || 0,
1879
1898
  'date_format' => @config['default_date_format'],
1880
1899
  'order' => @config['order'] || 'asc',
@@ -1886,19 +1905,19 @@ module Doing
1886
1905
  options[:interval_format] ||= cfg['interval_format'] || 'text'
1887
1906
 
1888
1907
  opts = {
1889
- section: section,
1890
- wrap_width: cfg['wrap_width'],
1908
+ case: options[:case],
1909
+ config_template: 'last',
1891
1910
  count: 1,
1892
- format: cfg['date_format'],
1893
- template: cfg['template'],
1894
- times: times,
1911
+ delete: options[:delete],
1895
1912
  duration: options[:duration],
1913
+ format: cfg['date_format'],
1896
1914
  interval_format: options[:interval_format],
1897
- case: options[:case],
1898
1915
  not: options[:negate],
1899
- config_template: 'last',
1900
- delete: options[:delete],
1901
- val: options[:val]
1916
+ section: section,
1917
+ template: options[:template] || cfg['template'],
1918
+ times: times,
1919
+ val: options[:val],
1920
+ wrap_width: cfg['wrap_width']
1902
1921
  }
1903
1922
 
1904
1923
  if options[:tag]
@@ -2204,7 +2223,7 @@ EOS
2204
2223
 
2205
2224
  filename ||= @config['doing_file']
2206
2225
  init_doing_file(filename)
2207
- current_content = @content.dup
2226
+ current_content = @content.clone
2208
2227
  backup_file = Util::Backup.last_backup(filename, count: 1)
2209
2228
  raise DoingRuntimeError, 'No undo history to diff' if backup_file.nil?
2210
2229
 
@@ -2307,7 +2326,6 @@ EOS
2307
2326
 
2308
2327
  section_items = @content.in_section(section)
2309
2328
  max = section_items.count - count.to_i
2310
- moved_items = []
2311
2329
 
2312
2330
  counter = 0
2313
2331
 
@@ -2324,8 +2342,9 @@ EOS
2324
2342
  item
2325
2343
  else
2326
2344
  counter += 1
2345
+ old_item = item.clone
2327
2346
  item.move_to(destination, label: label, log: false)
2328
- Hooks.trigger :post_entry_updated, self, item.dup
2347
+ Hooks.trigger :post_entry_updated, self, item, old_item
2329
2348
  item
2330
2349
  end
2331
2350
  end
data/lib/doing.rb CHANGED
@@ -23,12 +23,15 @@ require 'tty-markdown'
23
23
  require 'tty-reader'
24
24
  require 'tty-screen'
25
25
 
26
+ require_relative 'doing/changelog'
26
27
  require_relative 'doing/hash'
28
+ require_relative 'doing/types'
27
29
  require_relative 'doing/colors'
28
30
  require_relative 'doing/template_string'
29
31
  require_relative 'doing/string'
30
32
  require_relative 'doing/time'
31
33
  require_relative 'doing/array'
34
+ require_relative 'doing/good'
32
35
  require_relative 'doing/symbol'
33
36
  require_relative 'doing/util'
34
37
  require_relative 'doing/util_backup'
@@ -2,66 +2,23 @@
2
2
 
3
3
  require 'tty-spinner'
4
4
  require 'tty-progressbar'
5
- require './lib/doing'
6
5
  require 'open3'
7
6
  require 'shellwords'
7
+ require 'fileutils'
8
8
 
9
- class ::String
10
- include Doing::Color
11
-
12
- def highlight_errors
13
- cols = `tput cols`.strip.to_i
14
-
15
- string = dup
16
-
17
- errs = string.scan(/(?<==\n)(?:Failure|Error):.*?(?=\n=+)/m)
18
-
19
- errs.map! do |error|
20
- err = error.dup
21
-
22
- err.gsub!(%r{^(/.*?/)([^/:]+):(\d+):in (.*?)$}) do
23
- m = Regexp.last_match
24
- "#{m[1].white}#{m[2].bold.white}:#{m[3].yellow}:in #{m[4].cyan}"
25
- end
26
- err.gsub!(/(Failure|Error): (.*?)\((.*?)\):\n (.*?)(?=\n)/m) do
27
- m = Regexp.last_match
28
- [
29
- m[1].bold.boldbgred.white,
30
- m[3].bold.boldbgcyan.white,
31
- m[2].bold.boldbgyellow.black,
32
- " #{m[4]} ".bold.boldbgwhite.black.reset
33
- ].join(':'.boldblack.boldbgblack.reset)
34
- end
35
- err.gsub!(/(<.*?>) (was expected to) (.*?)\n( *<.*?>)./m) do
36
- m = Regexp.last_match
37
- "#{m[1].bold.green} #{m[2].white} #{m[3].boldwhite.boldbgred.reset}\n#{m[4].bold.white}"
38
- end
39
- err.gsub!(/(Finished in) ([\d.]+) (seconds)/) do
40
- m = Regexp.last_match
41
- "#{m[1].green} #{m[2].bold.white} #{m[3].green}"
42
- end
43
- err.gsub!(/(\d+) (failures)/) do
44
- m = Regexp.last_match
45
- "#{m[1].bold.red} #{m[2].red}"
46
- end
47
- err.gsub!(/100% passed/) do |m|
48
- m.bold.green
49
- end
50
-
51
- err
52
- end
53
-
54
- errs.join("\n#{('=' * cols).blue}\n")
55
- end
56
- end
9
+ $LOAD_PATH.unshift File.join(__dir__, '..')
10
+ require 'doing'
11
+ require 'helpers/threaded_tests_string'
57
12
 
58
13
  class ThreadedTests
59
14
  include Doing::Color
15
+ include ThreadedTestString
60
16
 
61
- def run(pattern: '*', max_threads: 24, max_tests: 0)
17
+ def run(pattern: '*', max_threads: 8, max_tests: 0)
62
18
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ @results = File.expand_path('results.log')
63
20
 
64
- max_threads = 24 if max_threads == 0
21
+ max_threads = 1000 if max_threads == 0
65
22
 
66
23
  c = Doing::Color
67
24
  c.coloring = true
@@ -90,12 +47,21 @@ class ThreadedTests
90
47
  ].join('')
91
48
  progress = TTY::ProgressBar::Multi.new(banner,
92
49
  width: 12,
50
+ clear: true,
93
51
  hide_cursor: true)
94
- children = []
52
+ @children = []
95
53
  tests.each do |t|
96
54
  test_name = File.basename(t, '.rb').sub(/doing_(.*?)_test/, '\1')
97
- new_sp = progress.register("[#{':bar'.cyan}] #{test_name.bold.white}:status", total: 2, width: 1, head: '.', hide_cursor: true)
98
- children.push([test_name, new_sp, nil])
55
+ new_sp = progress.register("[#{':bar'.cyan}] #{test_name.bold.white}:status",
56
+ total: tests.count + 8,
57
+ width: 1,
58
+ head: ' ',
59
+ unknown: ' ',
60
+ hide_cursor: true,
61
+ clear: true)
62
+ status = ': waiting'.dark.yellow.reset
63
+ @children.push([test_name, new_sp, status])
64
+ # new_sp.advance(status: ': waiting'.dark.yellow.reset)
99
65
  end
100
66
 
101
67
  @elapsed = 0.0
@@ -103,57 +69,21 @@ class ThreadedTests
103
69
  @assrt_total = 0
104
70
  @error_out = []
105
71
  # progress.start
72
+ @threads = []
73
+ @running_tests = []
106
74
 
107
75
  begin
108
- while children.count.positive?
109
- threads = []
110
- slices = children.slice!(0, max_threads)
76
+ while @children.count.positive?
77
+
78
+ slices = @children.slice!(0, max_threads)
111
79
  slices.each { |c| c[1].start }
112
80
  slices.each do |s|
113
- bar = s[1]
114
- bar.advance(status: ": #{'running'.green}")
115
-
116
- threads << Thread.new do
117
- out, _err, status = Open3.capture3(ENV, 'rake', "test:#{s[0]}", stdin_data: nil)
118
- unless status.success?
119
- m = out.match(/(?<fail>\d+) failures, (?<err>\d+) errors/)
120
- status = ": #{m['fail'].bold.red} #{'failures'.red}, #{m['err'].bold.red} #{'errors'.red}"
121
- bar.update(head: '✖'.boldred)
122
- bar.advance(head: '✖'.boldred, status: status)
123
-
124
- # errs = out.scan(/(?:Failure|Error): [\w_]+\((?:.*?)\):(?:.*?)(?=\n=======)/m)
125
- @error_out.push(out.highlight_errors)
126
- bar.finish
127
-
128
- Thread.exit
129
- end
130
-
131
- time = out.match(/^Finished in (?<time>\d+\.\d+) seconds\./)
132
- count = out.match(/^(?<tests>\d+) tests, (?<assrt>\d+) assertions, (?<fails>\d+) failures, (?<errs>\d+) errors/)
133
- status = [
134
- ': ',
135
- count['tests'].green,
136
- '/',
137
- count['assrt'].cyan,
138
- # ' (',
139
- # count['fails'].to_i == 0 ? '-'.dark.white.reset : count['fails'].bold.red,
140
- # '/',
141
- # count['errs'].to_i == 0 ? '-'.dark.white.reset : count['errs'].bold.red,
142
- # ') ',
143
- ' ',
144
- time['time'].to_f.round(3).to_s.yellow,
145
- 's'
146
- ].join('')
147
- bar.update(head: '✔'.boldgreen)
148
- bar.advance(head: '✔'.boldgreen, status: status)
149
- @test_total += count['tests'].to_i
150
- @assrt_total += count['assrt'].to_i
151
- @elapsed += time['time'].to_f
152
-
153
- bar.finish
81
+ @threads << Thread.new do
82
+ run_test(s)
154
83
  end
155
84
  end
156
- threads.each { |t| t.join }
85
+
86
+ @threads.each { |t| t.join }
157
87
  end
158
88
 
159
89
  finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -175,6 +105,83 @@ class ThreadedTests
175
105
  rescue
176
106
  progress.stop
177
107
  end
108
+ ensure
109
+ FileUtils.rm(@results)
110
+ end
111
+
112
+ def run_test(s)
113
+ bar = s[1]
114
+ s[2] = ": #{'running'.green}"
115
+ bar.advance(status: s[2])
116
+
117
+ if @running_tests.count.positive?
118
+ @running_tests.each do |b|
119
+ prev_bar = b[1]
120
+ if prev_bar.complete?
121
+ prev_bar.reset
122
+ prev_bar.advance(status: b[2])
123
+ prev_bar.finish
124
+ else
125
+ prev_bar.update(head: ' ', unfinished: ' ')
126
+ prev_bar.advance(status: b[2])
127
+ end
128
+ end
129
+ end
130
+
131
+ @running_tests.push(s)
132
+ out, _err, status = Open3.capture3(ENV, "rake test:#{s[0]} | tee #{@results}", stdin_data: nil)
133
+ unless status.success?
134
+ m = out.match(/(?<fail>\d+) failures, (?<err>\d+) errors/)
135
+ s[2] = ": #{m['fail'].bold.red} #{'failures'.red}, #{m['err'].bold.red} #{'errors'.red}"
136
+ bar.update(head: '✖'.boldred)
137
+ bar.advance(head: '✖'.boldred, status: s[2])
138
+
139
+ # errs = out.scan(/(?:Failure|Error): [\w_]+\((?:.*?)\):(?:.*?)(?=\n=======)/m)
140
+ @error_out.push(out.highlight_errors)
141
+ bar.finish
142
+
143
+ next_test
144
+ Thread.exit
145
+ end
146
+
147
+ time = out.match(/^Finished in (?<time>\d+\.\d+) seconds\./)
148
+ count = out.match(/^(?<tests>\d+) tests, (?<assrt>\d+) assertions, (?<fails>\d+) failures, (?<errs>\d+) errors/)
149
+ s[2] = [
150
+ ': ',
151
+ count['tests'].green,
152
+ '/',
153
+ count['assrt'].cyan,
154
+ # ' (',
155
+ # count['fails'].to_i == 0 ? '-'.dark.white.reset : count['fails'].bold.red,
156
+ # '/',
157
+ # count['errs'].to_i == 0 ? '-'.dark.white.reset : count['errs'].bold.red,
158
+ # ') ',
159
+ ' ',
160
+ time['time'].to_f.round(3).to_s.yellow,
161
+ 's'
162
+ ].join('')
163
+ bar.update(head: '✔'.boldgreen)
164
+ bar.advance(head: '✔'.boldgreen, status: s[2])
165
+ @test_total += count['tests'].to_i
166
+ @assrt_total += count['assrt'].to_i
167
+ @elapsed += time['time'].to_f
168
+
169
+ bar.finish
170
+
171
+ next_test
172
+ end
173
+
174
+ def next_test
175
+ if @children.count.positive?
176
+ t = Thread.new do
177
+ s = @children.shift
178
+ # s[1].start
179
+ # s[1].advance(status: ": #{'running'.green}")
180
+ run_test(s)
181
+ end
182
+
183
+ t.join
184
+ end
178
185
  end
179
186
  end
180
187
 
@@ -0,0 +1,50 @@
1
+ module ThreadedTestString
2
+ class ::String
3
+ include Doing::Color
4
+
5
+ def highlight_errors
6
+ cols = `tput cols`.strip.to_i
7
+
8
+ string = dup
9
+
10
+ errs = string.scan(/(?<==\n)(?:Failure|Error):.*?(?=\n=+)/m)
11
+
12
+ errs.map! do |error|
13
+ err = error.dup
14
+
15
+ err.gsub!(%r{^(/.*?/)([^/:]+):(\d+):in (.*?)$}) do
16
+ m = Regexp.last_match
17
+ "#{m[1].white}#{m[2].bold.white}:#{m[3].yellow}:in #{m[4].cyan}"
18
+ end
19
+ err.gsub!(/(Failure|Error): (.*?)\((.*?)\):\n (.*?)(?=\n)/m) do
20
+ m = Regexp.last_match
21
+ [
22
+ m[1].bold.boldbgred.white,
23
+ m[3].bold.boldbgcyan.white,
24
+ m[2].bold.boldbgyellow.black,
25
+ " #{m[4]} ".bold.boldbgwhite.black.reset
26
+ ].join(':'.boldblack.boldbgblack.reset)
27
+ end
28
+ err.gsub!(/(<.*?>) (was expected to) (.*?)\n( *<.*?>)./m) do
29
+ m = Regexp.last_match
30
+ "#{m[1].bold.green} #{m[2].white} #{m[3].boldwhite.boldbgred.reset}\n#{m[4].bold.white}"
31
+ end
32
+ err.gsub!(/(Finished in) ([\d.]+) (seconds)/) do
33
+ m = Regexp.last_match
34
+ "#{m[1].green} #{m[2].bold.white} #{m[3].green}"
35
+ end
36
+ err.gsub!(/(\d+) (failures)/) do
37
+ m = Regexp.last_match
38
+ "#{m[1].bold.red} #{m[2].red}"
39
+ end
40
+ err.gsub!(/100% passed/) do |m|
41
+ m.bold.green
42
+ end
43
+
44
+ err
45
+ end
46
+
47
+ errs.join("\n#{('=' * cols).blue}\n")
48
+ end
49
+ end
50
+ end