You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

283 lines
12 KiB

  1. """test_adjusters.py -- test pipeline Adjusters (eg TargetTimeAdjuster)"""
  2. import unittest
  3. import unittest.mock as mock
  4. import pipeline.adjusters as adjusters
  5. from test.mocks import MockFeature, MockInterval
  6. class TestAdjuster(unittest.TestCase):
  7. """Test the generic Adjuster class"""
  8. def test_init(self):
  9. """Test the Adjuster can be initialised"""
  10. adjuster = adjusters.Adjuster()
  11. self.assertEqual(adjuster.features, [])
  12. def test_adjust(self):
  13. """Test the generic adjust"""
  14. adjuster = adjusters.Adjuster()
  15. self.assertEqual(adjuster.adjust(), [])
  16. self.assertEqual(adjuster.features, [])
  17. class TestTargetTimeAdjuster(unittest.TestCase):
  18. """Test the TargetTimeAdjuster
  19. TTA drops Features until the target time is reached (or within a margin)"""
  20. def test_init(self):
  21. """Test the TTA can be initialised"""
  22. tta = adjusters.TargetTimeAdjuster()
  23. self.assertEqual(tta.features, [])
  24. def test_features_total_time(self):
  25. """Test the TTA can calculate the total time of Features
  26. Test:
  27. - input duration floats: 1.0, 2.0, 3.0, 4.0 == 10.0
  28. """
  29. tta = adjusters.TargetTimeAdjuster()
  30. features = []
  31. for i in range(1, 5):
  32. features.append(make_feature(duration=i*1.0))
  33. self.assertEqual(tta._features_total_time(features), 10.0)
  34. self.assertEqual(tta._features_total_time([]), 0.0)
  35. self.assertIs(type(tta._features_total_time([])), float)
  36. def test_determine_margin(self):
  37. """Test the TTA can determine the target time margins
  38. Args: time, margin, strategy (strategy in: ABSOLUTE, PERCENT)
  39. Test:
  40. - margin of zero
  41. - margin of 5.0
  42. - margin of 10.0
  43. - margin of 100.0
  44. - both ABSOLUTE and PERCENT strategies
  45. TODO: figure out what should be done with negative margins & margins > 100.0
  46. """
  47. tta = adjusters.TargetTimeAdjuster()
  48. with self.subTest("ABSOLUTE"):
  49. strategy = adjusters.TargetTimeAdjuster._STRATEGY.ABSOLUTE
  50. test_cases = []
  51. # populate test cases with tuples of (time, margin, expected)
  52. # zero margin
  53. test_cases.append((60.0, 0.0, (60.0, 60.0)))
  54. # margin of 5.0
  55. test_cases.append((60.0, 5.0, (55.0, 65.0)))
  56. # margin of 10.0
  57. test_cases.append((60.0, 10.0, (50.0, 70.0)))
  58. # margin of 100.0
  59. test_cases.append((60.0, 100.0, (0.0, 160.0)))
  60. # test
  61. for time, margin, expected in test_cases:
  62. self.assertEqual(tta._determine_margin(time, margin, strategy), expected)
  63. with self.subTest("PERCENT"):
  64. strategy = adjusters.TargetTimeAdjuster._STRATEGY.PERCENT
  65. test_cases = []
  66. # populate test cases with tuples of (time, margin, expected) as above
  67. # zero margin
  68. test_cases.append((60.0, 0.0, (60.0, 60.0)))
  69. # margin of 5.0
  70. test_cases.append((60.0, 5.0, (57.0, 63.0)))
  71. # margin of 10.0
  72. test_cases.append((60.0, 10.0, (54.0, 66.0)))
  73. # margin of 100.0
  74. test_cases.append((60.0, 100.0, (0.0, 120.0)))
  75. # test
  76. for time, margin, expected in test_cases:
  77. self.assertEqual(tta._determine_margin(time, margin, strategy), expected)
  78. def test_adjust_no_change(self):
  79. """Test adjusting of list of Features using TTA -- no change to list of Features
  80. Cases:
  81. - no Features --> []
  82. - [Features] with total time < target time --> unchanged list
  83. - [Features] with total time = target time --> unchanged list
  84. TODO: test with Features > target
  85. """
  86. with self.subTest("no Features"):
  87. tta = adjusters.TargetTimeAdjuster()
  88. self.assertEqual(tta.adjust(), [])
  89. with self.subTest("Features < target time"):
  90. features = []
  91. for i in range(1, 5):
  92. features.append(make_feature(duration=i*1.0))
  93. tta = adjusters.TargetTimeAdjuster(features=features, target_time=20.0)
  94. self.assertEqual(tta.adjust(), features)
  95. with self.subTest("Features = target time"):
  96. features = []
  97. for i in range(1, 5):
  98. features.append(make_feature(duration=i*1.0))
  99. tta = adjusters.TargetTimeAdjuster(features=features, target_time=10.0)
  100. self.assertEqual(tta.adjust(), features)
  101. def test_sort_by_score_time(self):
  102. """Test sorting of list of Features by score (primary) and time (secondary)
  103. Cases:
  104. - [(15.0, 1.0), (10.0, 1.0), (12.0, 1.0)] --> [(10.0, 1.0), (12.0, 1.0), (15.0, 1.0)] # score equal, sort by time
  105. - [(15.0, 1.0), (10.0, 4.0), (12.0, 3.0)] --> [(15.0, 1.0), (12.0, 3.0), (10.0, 4.0)] # sort by score
  106. - [(15.0, 1.0), (10.0, 1.0), (12.0, 2.0)] --> [(10.0, 1.0), (15.0, 1.0), (12.0, 2.0)] # mixed: scores below duration
  107. - [] --> []
  108. - [(15.0, 1.0)] --> [(15.0, 1.0)]
  109. Cases giving RDH trouble:
  110. - [(16.0, 1.0), (16.0, 1.0), (1.0, 1.0), (1.0, 1.0)] --> [(1.0, 1.0), (1.0, 1.0), (16.0, 1.0), (16.0, 1.0)] # multiple lowest scoring, multiple shortest duration
  111. """
  112. tta = adjusters.TargetTimeAdjuster()
  113. with self.subTest("score equal, sort by duration"):
  114. features = [
  115. make_feature(duration=15.0, score=1.0),
  116. make_feature(duration=10.0, score=1.0),
  117. make_feature(duration=12.0, score=1.0)
  118. ]
  119. self.assertEqual(tta._sort_by_score_time(features), [features[1], features[2], features[0]])
  120. with self.subTest("sort by score, duration irrelevant"):
  121. features = [
  122. make_feature(duration=15.0, score=1.0),
  123. make_feature(duration=10.0, score=4.0),
  124. make_feature(duration=12.0, score=3.0)
  125. ]
  126. self.assertEqual(tta._sort_by_score_time(features), [features[0], features[2], features[1]])
  127. with self.subTest("mixed: scores below duration"):
  128. features = [
  129. make_feature(duration=15.0, score=1.0),
  130. make_feature(duration=10.0, score=1.0),
  131. make_feature(duration=12.0, score=2.0)
  132. ]
  133. self.assertEqual(tta._sort_by_score_time(features), [features[1], features[0], features[2]])
  134. with self.subTest("empty"):
  135. self.assertEqual(tta._sort_by_score_time([]), [])
  136. with self.subTest("single"):
  137. features = [mock.Mock(duration=15.0, score=1.0)]
  138. self.assertEqual(tta._sort_by_score_time(features), features)
  139. with self.subTest("multiple lowest scoring, multiple shortest duration"):
  140. features = [
  141. make_feature(duration=16.0, score=1.0),
  142. make_feature(duration=16.0, score=1.0),
  143. make_feature(duration=1.0, score=1.0),
  144. make_feature(duration=1.0, score=1.0)
  145. ]
  146. self.assertEqual(tta._sort_by_score_time(features), [features[2], features[3], features[0], features[1]])
  147. def test_adjust_changes(self):
  148. """Test adjusting of list of Features using TTA -- changes to list of Features
  149. All cases have total time > target time.
  150. In the cases, specification is Feature(duration, score)
  151. Cases:
  152. - target = 30.0, margin = 0.0
  153. + [(15.0, 1.0), (10.0, 1.0), (12.0, 1.0)] --> [(15.0, 1.0), (12.0, 1.0)] # scores equal, drop smallest
  154. + [(15.0, 2.0), (10.0, 2.0), (12.0, 1.0)] --> [(15.0, 1.0), (10.0, 1.0)] # drop lowest scoring (1)
  155. + [(15.0, 1.0), (10.0, 1.0), (12.0, 2.0)] --> [(15.0, 1.0), (12.0, 2.0)] # drop lowest scoring (2)
  156. - target = 30.0, margin = 4.0
  157. + [(15.0, 1.0), (10.0, 2.0), (12.0, 1.0)] --> [(15.0, 1.0), (12.0, 1.0)] # not lowest scoring, but within margin
  158. + [(16.0, 1.0), (16.0, 1.0), (1.0, 1.0), (1.0, 1.0)] --> [(16.0, 1.0), (16.0, 1.0)] # drop multiple lowest scoring, shortest duration
  159. """
  160. # target 30.0, margin 0.0 cases
  161. target, margin = 30.0, 0.0
  162. with self.subTest(f"target {target} margin {margin}"):
  163. with self.subTest("scores equal"):
  164. features = [
  165. make_feature(duration=15.0, score=1.0),
  166. make_feature(duration=10.0, score=1.0),
  167. make_feature(duration=12.0, score=1.0)
  168. ]
  169. tta = adjusters.TargetTimeAdjuster(features=features, target_time=target, margin=margin)
  170. expected = [features[0], features[2]]
  171. output = tta.adjust()
  172. self.assertEqual(len(output), 2)
  173. self.assertEqual(output, expected)
  174. self.assertEqual(tta.features, expected)
  175. with self.subTest("drop lowest scoring (1)"):
  176. features = [
  177. make_feature(duration=15.0, score=2.0),
  178. make_feature(duration=10.0, score=2.0),
  179. make_feature(duration=12.0, score=1.0)
  180. ]
  181. tta = adjusters.TargetTimeAdjuster(features=features, target_time=target, margin=margin)
  182. expected = [features[0], features[1]]
  183. output = tta.adjust()
  184. self.assertEqual(len(output), 2)
  185. self.assertEqual(output, expected)
  186. self.assertEqual(tta.features, expected)
  187. with self.subTest("drop lowest scoring (2)"):
  188. features = [
  189. make_feature(duration=15.0, score=1.0),
  190. make_feature(duration=10.0, score=1.0),
  191. make_feature(duration=12.0, score=2.0)
  192. ]
  193. tta = adjusters.TargetTimeAdjuster(features=features, target_time=target, margin=margin)
  194. expected = [features[0], features[2]]
  195. output = tta.adjust()
  196. self.assertEqual(len(output), 2)
  197. self.assertEqual(output, expected)
  198. self.assertEqual(tta.features, expected)
  199. # target 30.0, margin 4.0 cases
  200. target, margin, strategy = 30.0, 4.0, adjusters.TargetTimeAdjuster._STRATEGY.ABSOLUTE
  201. with self.subTest(f"target {target} margin {margin}"):
  202. with self.subTest("not lowest scoring, but within margin"):
  203. # explanation: dropping the 10.0 feature would put us at 27.0, which is within the margin (26.0, 34.0)
  204. features = [
  205. make_feature(duration=15.0, score=1.0),
  206. make_feature(duration=10.0, score=2.0),
  207. make_feature(duration=12.0, score=1.0)
  208. ]
  209. tta = adjusters.TargetTimeAdjuster(features=features, target_time=target,
  210. margin=margin, strategy=strategy)
  211. expected = [features[0], features[2]]
  212. output = tta.adjust()
  213. self.assertEqual(len(output), 2)
  214. self.assertEqual(output, expected)
  215. self.assertEqual(tta.features, expected)
  216. with self.subTest("drop multiple lowest scoring, shortest duration"):
  217. # explanation: dropping the 1.0 features would put us at 32.0, which is within the margin (26.0, 34.0)
  218. features = [
  219. make_feature(duration=16.0, score=1.0),
  220. make_feature(duration=16.0, score=1.0),
  221. make_feature(duration=1.0, score=1.0),
  222. make_feature(duration=1.0, score=1.0),
  223. make_feature(duration=1.0, score=1.0),
  224. make_feature(duration=1.0, score=1.0)
  225. ]
  226. tta = adjusters.TargetTimeAdjuster(features=features, target_time=target,
  227. margin=margin, strategy=strategy)
  228. expected = [features[0], features[1], features[2], features[3]]
  229. output = tta.adjust()
  230. self.assertEqual(len(output), 4)
  231. self.assertEqual(output, expected)
  232. self.assertEqual(tta.features, expected)
  233. def make_feature(duration, score=1.0):
  234. """Helper function to create a MockFeature from duration and score"""
  235. return MockFeature(interval=MockInterval.from_duration(duration), score=score)