| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import decimal |
| import unittest |
| |
| from bad_machine_finder import detection |
| from bad_machine_finder import tasks |
| |
| |
| class BadMachineListUnittest(unittest.TestCase): |
| |
| def testBasic(self): |
| """Tests basic functionality of the class.""" |
| first_list = detection.BadMachineList() |
| first_list.AddBadMachine('bot-1', 'reason-1') |
| first_list.AddBadMachine('bot-2', 'reason-2') |
| |
| second_list = detection.BadMachineList() |
| second_list.AddBadMachine('bot-2', 'reason-3') |
| second_list.AddBadMachine('bot-2', 'reason-4') |
| |
| first_list.Merge(second_list) |
| expected_bad_machines = { |
| 'bot-1': [ |
| 'reason-1', |
| ], |
| 'bot-2': [ |
| 'reason-2', |
| 'reason-3', |
| 'reason-4', |
| ], |
| } |
| |
| self.assertEqual(first_list.bad_machines, expected_bad_machines) |
| |
| def testRemoveLowConfidenceMachines(self): |
| """Tests that low confidence machines are correctly removed.""" |
| bad_machine_list = detection.BadMachineList() |
| bad_machine_list.AddBadMachine('bot-1', 'reason-1') |
| bad_machine_list.AddBadMachine('bot-2', 'reason-2') |
| bad_machine_list.AddBadMachine('bot-2', 'reason-3') |
| |
| expected_bad_machines = { |
| 'bot-1': ['reason-1'], |
| 'bot-2': [ |
| 'reason-2', |
| 'reason-3', |
| ], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| bad_machine_list.RemoveLowConfidenceMachines(2) |
| expected_bad_machines = { |
| 'bot-2': [ |
| 'reason-2', |
| 'reason-3', |
| ], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testIterMarkdown(self): |
| """Tests that Markdown output is correct.""" |
| bad_machine_list = detection.BadMachineList() |
| bad_machine_list.AddBadMachine('bot-2', 'reason-2') |
| bad_machine_list.AddBadMachine('bot-2', 'reason-3') |
| bad_machine_list.AddBadMachine('bot-1', 'reason-1') |
| |
| bot_1_expected_markdown = """\ |
| * bot-1 |
| * reason-1""" |
| bot_2_expected_markdown = """\ |
| * bot-2 |
| * reason-2 |
| * reason-3""" |
| |
| expected_pairs = [ |
| ('bot-1', bot_1_expected_markdown), |
| ('bot-2', bot_2_expected_markdown), |
| ] |
| self.assertEqual(list(bad_machine_list.IterMarkdown()), expected_pairs) |
| |
| |
| class MixinGroupedBadMachinesUnittest(unittest.TestCase): |
| |
| def testInputValidation(self): |
| """Tests that invalid inputs are properly caught.""" |
| bad_machine_list = detection.BadMachineList() |
| mgbm = detection.MixinGroupedBadMachines() |
| mgbm.AddMixinData('mixin_name', bad_machine_list) |
| with self.assertRaisesRegex( |
| ValueError, 'Bad machines for mixin mixin_name were already added'): |
| mgbm.AddMixinData('mixin_name', bad_machine_list) |
| |
| def testGetAllBadMachineNames(self): |
| """Tests that all bad machine names are properly returned.""" |
| first_bad_machine_list = detection.BadMachineList() |
| first_bad_machine_list.AddBadMachine('bot-1', 'reason-1') |
| first_bad_machine_list.AddBadMachine('bot-2', 'reason-2') |
| |
| second_bad_machine_list = detection.BadMachineList() |
| second_bad_machine_list.AddBadMachine('bot-2', 'reason-3') |
| second_bad_machine_list.AddBadMachine('bot-3', 'reason-4') |
| |
| mgbm = detection.MixinGroupedBadMachines() |
| mgbm.AddMixinData('first', first_bad_machine_list) |
| mgbm.AddMixinData('second', second_bad_machine_list) |
| |
| self.assertEqual(mgbm.GetAllBadMachineNames(), {'bot-1', 'bot-2', 'bot-3'}) |
| |
| def testGenerateMarkdown(self): |
| """Tests basic Markdown generation.""" |
| first_bad_machine_list = detection.BadMachineList() |
| first_bad_machine_list.AddBadMachine('bot-1', 'reason-1a') |
| first_bad_machine_list.AddBadMachine('bot-1', 'reason-1b') |
| first_bad_machine_list.AddBadMachine('bot-2', 'reason-2') |
| |
| second_bad_machine_list = detection.BadMachineList() |
| second_bad_machine_list.AddBadMachine('bot-3', 'reason-3') |
| second_bad_machine_list.AddBadMachine('bot-4', 'reason-4') |
| |
| mgbm = detection.MixinGroupedBadMachines() |
| mgbm.AddMixinData('mixin-b', second_bad_machine_list) |
| mgbm.AddMixinData('mixin-a', first_bad_machine_list) |
| |
| expected_markdown = """\ |
| Bad machines for mixin-a |
| * bot-1 |
| * reason-1a |
| * reason-1b |
| * bot-2 |
| * reason-2 |
| |
| Bad machines for mixin-b |
| * bot-3 |
| * reason-3 |
| * bot-4 |
| * reason-4""" |
| self.assertEqual(mgbm.GenerateMarkdown(), expected_markdown) |
| |
| def testGenerateMarkdownWithSomeBotsSkipped(self): |
| """Tests Markdown generation when some bots should be skipped.""" |
| first_bad_machine_list = detection.BadMachineList() |
| first_bad_machine_list.AddBadMachine('bot-1', 'reason-1a') |
| first_bad_machine_list.AddBadMachine('bot-1', 'reason-1b') |
| first_bad_machine_list.AddBadMachine('bot-2', 'reason-2') |
| |
| second_bad_machine_list = detection.BadMachineList() |
| second_bad_machine_list.AddBadMachine('bot-3', 'reason-3') |
| second_bad_machine_list.AddBadMachine('bot-4', 'reason-4') |
| |
| mgbm = detection.MixinGroupedBadMachines() |
| mgbm.AddMixinData('mixin-b', second_bad_machine_list) |
| mgbm.AddMixinData('mixin-a', first_bad_machine_list) |
| |
| expected_markdown = """\ |
| Bad machines for mixin-a |
| * bot-2 |
| * reason-2""" |
| self.assertEqual( |
| mgbm.GenerateMarkdown(bots_to_skip={'bot-1', 'bot-3', 'bot-4'}), |
| expected_markdown) |
| |
| def testGenerateMarkdownWithAllBotsSkipped(self): |
| """Tests Markdown generation when all bots should be skipped.""" |
| first_bad_machine_list = detection.BadMachineList() |
| first_bad_machine_list.AddBadMachine('bot-1', 'reason-1a') |
| first_bad_machine_list.AddBadMachine('bot-1', 'reason-1b') |
| first_bad_machine_list.AddBadMachine('bot-2', 'reason-2') |
| |
| second_bad_machine_list = detection.BadMachineList() |
| second_bad_machine_list.AddBadMachine('bot-3', 'reason-3') |
| second_bad_machine_list.AddBadMachine('bot-4', 'reason-4') |
| |
| mgbm = detection.MixinGroupedBadMachines() |
| mgbm.AddMixinData('mixin-b', second_bad_machine_list) |
| mgbm.AddMixinData('mixin-a', first_bad_machine_list) |
| |
| self.assertEqual( |
| mgbm.GenerateMarkdown( |
| bots_to_skip={'bot-1', 'bot-2', 'bot-3', 'bot-4'}), '') |
| |
| |
| class DetectViaStdDevOutlierUnittest(unittest.TestCase): |
| |
| def testInputChecking(self): |
| """Tests that invalid inputs are checked.""" |
| # No tasks. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.Freeze() |
| with self.assertRaises(ValueError): |
| detection.DetectViaStdDevOutlier(mixin_stats, 2, 0) |
| |
| # Negative threshold |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bot', 'suite', 1, 0) |
| mixin_stats.Freeze() |
| with self.assertRaises(ValueError): |
| detection.DetectViaStdDevOutlier(mixin_stats, -1, 0) |
| |
| def testSmallGoodFleet(self): |
| mixin_stats = tasks.MixinStats() |
| for i in range(10): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, i % 5) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaStdDevOutlier(mixin_stats, 2, 0) |
| self.assertEqual(bad_machine_list.bad_machines, {}) |
| |
| def testOneClearlyBadMachineSmallFleet(self): |
| """Tests behavior when there is a single clearly bad machine.""" |
| # We need enough samples that the mean is sufficiently skewed towards the |
| # good bots' failure rate for this detection method to work. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bad-bot', 'suite', 100, 99) |
| for i in range(10): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, 1) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaStdDevOutlier(mixin_stats, 2, 0) |
| expected_bad_machines = { |
| 'bad-bot': [('Had a failure rate of 0.99 despite a fleet-wide average ' |
| 'of 0.09909090909090909 and a standard deviation of ' |
| '0.2817301915422738.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testSeveralBadMachinesLargeFleet(self): |
| """Tests behavior when there are several bad machines in a large fleet.""" |
| mixin_stats = tasks.MixinStats() |
| for i in range(98): |
| mixin_stats.AddStatsForBotAndSuite(f'bot-{i}', 'suite', 100, 1) |
| mixin_stats.AddStatsForBotAndSuite('bot-98', 'suite', 100, 15) |
| mixin_stats.AddStatsForBotAndSuite('bot-99', 'suite', 100, 20) |
| mixin_stats.AddStatsForBotAndSuite('bot-100', 'suite', 100, 50) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaStdDevOutlier(mixin_stats, 2, 0) |
| expected_bad_machines = { |
| 'bot-98': [('Had a failure rate of 0.15 despite a fleet-wide average ' |
| 'of 0.018118811881188118 and a standard deviation of ' |
| '0.05350511905346074.')], |
| 'bot-99': [('Had a failure rate of 0.2 despite a fleet-wide average ' |
| 'of 0.018118811881188118 and a standard deviation of ' |
| '0.05350511905346074.')], |
| 'bot-100': [('Had a failure rate of 0.5 despite a fleet-wide average ' |
| 'of 0.018118811881188118 and a standard deviation of ' |
| '0.05350511905346074.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testSmallFlakyFleet(self): |
| """Tests behavior when there's a bad machine in a small, flaky fleet.""" |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bad-bot', 'suite', 100, 50) |
| for i in range(9): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, 25) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaStdDevOutlier(mixin_stats, 2, 0) |
| expected_bad_machines = { |
| 'bad-bot': [('Had a failure rate of 0.5 despite a fleet-wide average ' |
| 'of 0.275 and a standard deviation of 0.075.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testLowFailedTaskMachinesSkipped(self): |
| """Tests that machines w/ low failed task counts are skipped.""" |
| mixin_stats = tasks.MixinStats() |
| for i in range(98): |
| mixin_stats.AddStatsForBotAndSuite(f'bot-{i}', 'suite', 100, 1) |
| mixin_stats.AddStatsForBotAndSuite('bot-98', 'suite', 10, 3) |
| mixin_stats.AddStatsForBotAndSuite('bot-99', 'suite', 100, 50) |
| mixin_stats.Freeze() |
| |
| with self.assertLogs(level='DEBUG') as log_manager: |
| bad_machine_list = detection.DetectViaStdDevOutlier(mixin_stats, 2, 5) |
| for line in log_manager.output: |
| if ('Bot bot-98 skipped in DetectViaStdDevOutlier due to only having 3 ' |
| 'failed tasks') in line: |
| break |
| else: |
| self.fail('Did not find expected log line') |
| |
| expected_bad_machines = { |
| 'bot-99': [('Had a failure rate of 0.5 despite a fleet-wide average ' |
| 'of 0.0178 and a standard deviation of ' |
| '0.05640177302177654.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| |
| class DetectViaRandomChanceUnittest(unittest.TestCase): |
| |
| def testInputChecking(self): |
| """Tests that invalid inputs are checked.""" |
| # No tasks. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.Freeze() |
| with self.assertRaises(ValueError): |
| detection.DetectViaRandomChance(mixin_stats, 0.005) |
| |
| # Non-positive probability. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bot', 'suite', 1, 0) |
| mixin_stats.Freeze() |
| with self.assertRaises(ValueError): |
| detection.DetectViaRandomChance(mixin_stats, 0) |
| |
| # >1 probability |
| with self.assertRaises(ValueError): |
| detection.DetectViaRandomChance(mixin_stats, 1.1) |
| |
| def testSmallGoodFleet(self): |
| """Tests behavior when there are no bad machines.""" |
| mixin_stats = tasks.MixinStats() |
| for i in range(10): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, i % 5) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaRandomChance(mixin_stats, 0.005) |
| self.assertEqual(bad_machine_list.bad_machines, {}) |
| |
| def testOneClearlyBadMachineSmallFleet(self): |
| """Tests behavior when there is a single clearly bad machine.""" |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bad-bot', 'suite', 100, 99) |
| mixin_stats.AddStatsForBotAndSuite('good-bot', 'suite', 100, 1) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaRandomChance(mixin_stats, 0.005) |
| expected_bad_machines = { |
| 'bad-bot': [('99 of 100 tasks failed despite a fleet-wide average ' |
| 'failed task rate of 0.5. The probability of this ' |
| 'happening randomly is 7.967495142732219e-29.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testSeveralBadMachinesLargeFleet(self): |
| """Tests behavior when there are several bad machines in a large fleet.""" |
| mixin_stats = tasks.MixinStats() |
| for i in range(98): |
| mixin_stats.AddStatsForBotAndSuite(f'bot-{i}', 'suite', 100, 1) |
| mixin_stats.AddStatsForBotAndSuite('bot-98', 'suite', 100, 15) |
| mixin_stats.AddStatsForBotAndSuite('bot-99', 'suite', 100, 20) |
| mixin_stats.AddStatsForBotAndSuite('bot-100', 'suite', 100, 50) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaRandomChance(mixin_stats, 0.005) |
| expected_bad_machines = { |
| 'bot-98': [('15 of 100 tasks failed despite a fleet-wide average ' |
| 'failed task rate of 0.01811881188118811881188118812. ' |
| 'The probability of this happening randomly is ' |
| '4.41689373707857e-10.')], |
| 'bot-99': [('20 of 100 tasks failed despite a fleet-wide average ' |
| 'failed task rate of 0.01811881188118811881188118812. The ' |
| 'probability of this happening randomly is ' |
| '1.9407812867119233e-15.')], |
| 'bot-100': [('50 of 100 tasks failed despite a fleet-wide average ' |
| 'failed task rate of 0.01811881188118811881188118812. The ' |
| 'probability of this happening randomly is ' |
| '3.3205488374477226e-59.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testSmallFlakyFleet(self): |
| """Tests behavior when there's a bad machine in a small, flaky fleet.""" |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bad-bot', 'suite', 100, 50) |
| for i in range(9): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, 25) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaRandomChance(mixin_stats, 0.005) |
| expected_bad_machines = { |
| 'bad-bot': [('50 of 100 tasks failed despite a fleet-wide average ' |
| 'failed task rate of 0.275. The probability of this ' |
| 'happening randomly is 1.5273539960703075e-06.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| |
| class DetectViaInterquartileRangeUnittest(unittest.TestCase): |
| |
| def testInputChecking(self): |
| """Tests that invalid inputs are checked.""" |
| # No tasks. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.Freeze() |
| with self.assertRaises(ValueError): |
| detection.DetectViaInterquartileRange(mixin_stats, 'mixin_name', 1.5, 0) |
| |
| # Non-positive iqr_multiplier. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bot', 'suite', 1, 0) |
| mixin_stats.Freeze() |
| with self.assertRaises(ValueError): |
| detection.DetectViaInterquartileRange(mixin_stats, 'mixin_name', 0, 0) |
| |
| # Less than the number of samples needed for quartiles to be meaningful. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bot-1', 'suite', 99, 0) |
| mixin_stats.AddStatsForBotAndSuite('bot-2', 'suite', 1, 0) |
| mixin_stats.AddStatsForBotAndSuite('bot-3', 'suite', 1, 0) |
| mixin_stats.AddStatsForBotAndSuite('bot-4', 'suite', 1, 0) |
| mixin_stats.Freeze() |
| |
| with self.assertLogs(level='INFO') as log_manager: |
| bad_machine_list = detection.DetectViaInterquartileRange( |
| mixin_stats, 'mixin_name', 1.5, 0) |
| for line in log_manager.output: |
| if ('Quartiles require at least 5 samples to be meaningful. Mixin ' |
| 'mixin_name only provided 4 samples.') in line: |
| break |
| else: |
| self.fail('Did not find expected log line') |
| self.assertEqual(bad_machine_list.bad_machines, {}) |
| |
| # IQR ends up being 0. |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bot-1', 'suite', 99, 0) |
| mixin_stats.AddStatsForBotAndSuite('bot-2', 'suite', 1, 0) |
| mixin_stats.AddStatsForBotAndSuite('bot-3', 'suite', 1, 0) |
| mixin_stats.AddStatsForBotAndSuite('bot-4', 'suite', 1, 0) |
| mixin_stats.AddStatsForBotAndSuite('bot-5', 'suite', 1, 0) |
| mixin_stats.Freeze() |
| |
| with self.assertLogs(level='INFO') as log_manager: |
| bad_machine_list = detection.DetectViaInterquartileRange( |
| mixin_stats, 'mixin_name', 1.5, 0) |
| for line in log_manager.output: |
| if ('Mixin mixin_name resulted in an IQR of 0, which is not useful for ' |
| 'detecting outliers.') in line: |
| break |
| else: |
| self.fail('Did not find expected log line') |
| self.assertEqual(bad_machine_list.bad_machines, {}) |
| |
| def testSmallGoodFleet(self): |
| """Tests behavior when there are no bad machines.""" |
| mixin_stats = tasks.MixinStats() |
| for i in range(10): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, i % 5) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaInterquartileRange( |
| mixin_stats, 'mixin_name', 1.5, 0) |
| self.assertEqual(bad_machine_list.bad_machines, {}) |
| |
| def testOneClearlyBadMachineSmallFleet(self): |
| """Tests behavior when there is a single clearly bad machine.""" |
| mixin_stats = tasks.MixinStats() |
| # We need > 4 samples in order for quartiles to be meaningful. |
| mixin_stats.AddStatsForBotAndSuite('bad-bot', 'suite', 100, 99) |
| for i in range(10): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, i % 5) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaInterquartileRange( |
| mixin_stats, 'mixin_name', 1.5, 0) |
| expected_bad_machines = { |
| 'bad-bot': [('Failure rate of 0.99 is above the IQR-based upper bound ' |
| 'of 0.07250000000000001.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testSeveralBadMachinesLargeFleet(self): |
| """Tests behavior when there are several bad machines in a large fleet.""" |
| mixin_stats = tasks.MixinStats() |
| for i in range(98): |
| mixin_stats.AddStatsForBotAndSuite(f'bot-{i}', 'suite', 100, i % 5) |
| mixin_stats.AddStatsForBotAndSuite('bot-98', 'suite', 100, 15) |
| mixin_stats.AddStatsForBotAndSuite('bot-99', 'suite', 100, 20) |
| mixin_stats.AddStatsForBotAndSuite('bot-100', 'suite', 100, 50) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaInterquartileRange( |
| mixin_stats, 'mixin_name', 1.5, 0) |
| expected_bad_machines = { |
| 'bot-98': [('Failure rate of 0.15 is above the IQR-based upper bound ' |
| 'of 0.06.')], |
| 'bot-99': [('Failure rate of 0.2 is above the IQR-based upper bound ' |
| 'of 0.06.')], |
| 'bot-100': [('Failure rate of 0.5 is above the IQR-based upper bound ' |
| 'of 0.06.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testSmallFlakyFleet(self): |
| """Tests behavior when there's a bad machine in a small, flaky fleet.""" |
| mixin_stats = tasks.MixinStats() |
| mixin_stats.AddStatsForBotAndSuite('bad-bot', 'suite', 100, 50) |
| for i in range(9): |
| mixin_stats.AddStatsForBotAndSuite(f'good-bot-{i}', 'suite', 100, |
| 25 + i % 5) |
| mixin_stats.Freeze() |
| |
| bad_machine_list = detection.DetectViaInterquartileRange( |
| mixin_stats, 'mixin_name', 1.5, 0) |
| expected_bad_machines = { |
| 'bad-bot': [('Failure rate of 0.5 is above the IQR-based upper bound ' |
| 'of 0.31000000000000005.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| def testLowFailedTaskMachinesSkipped(self): |
| """Tests that machines w/ low failed task counts are skipped.""" |
| mixin_stats = tasks.MixinStats() |
| for i in range(98): |
| mixin_stats.AddStatsForBotAndSuite(f'bot-{i}', 'suite', 100, i % 3) |
| mixin_stats.AddStatsForBotAndSuite('bot-98', 'suite', 10, 4) |
| mixin_stats.AddStatsForBotAndSuite('bot-99', 'suite', 100, 50) |
| mixin_stats.Freeze() |
| |
| with self.assertLogs(level='DEBUG') as log_manager: |
| bad_machine_list = detection.DetectViaInterquartileRange( |
| mixin_stats, 'mixin_name', 1.5, 5) |
| for line in log_manager.output: |
| if ('Bot bot-98 skipped in DetectViaInterquartileRange due to only ' |
| 'having 4 failed tasks') in line: |
| break |
| else: |
| self.fail('Did not find expected log line') |
| |
| expected_bad_machines = { |
| 'bot-99': [('Failure rate of 0.5 is above the IQR-based upper bound of ' |
| '0.05.')], |
| } |
| self.assertEqual(bad_machine_list.bad_machines, expected_bad_machines) |
| |
| |
| class IndependentEventHelpersUnittest(unittest.TestCase): |
| |
| def testChanceOfExactlyNIndependentEvents(self): |
| """Tests behavior of the N independent events helper.""" |
| # pylint: disable=protected-access |
| func = detection._ChanceOfExactlyNIndependentEvents |
| # pylint: enable=protected-access |
| |
| # Equivalent to flipping a coin and getting heads. |
| self.assertEqual(func(decimal.Decimal(0.5), 1, 1), decimal.Decimal(0.5)) |
| |
| # Equivalent to flipping two coins and getting zero heads. |
| self.assertEqual(func(decimal.Decimal(0.5), 2, 0), decimal.Decimal(0.25)) |
| |
| # Make sure that these are special-cased since Decimal(0)**0 is an error. |
| self.assertEqual(func(decimal.Decimal(0), 2, 1), decimal.Decimal(0)) |
| self.assertEqual(func(decimal.Decimal(0), 2, 0), decimal.Decimal(1)) |
| self.assertEqual(func(decimal.Decimal(1), 2, 1), decimal.Decimal(0)) |
| self.assertEqual(func(decimal.Decimal(1), 2, 2), decimal.Decimal(1)) |
| |
| def testChanceOfNOrMoreIndependentEvents(self): |
| """Tests behavior of the N+ independent events helper.""" |
| # pylint: disable=protected-access |
| func = detection._ChanceOfNOrMoreIndependentEvents |
| # pylint: enable=protected-access |
| |
| # Probability of getting 0 or more should always be 1. |
| self.assertEqual(func(decimal.Decimal(0.5), 10, 0), 1) |
| |
| # Equivalent to flipping two coins and getting at least one heads. |
| self.assertEqual(func(decimal.Decimal(0.5), 2, 1), 0.75) |