• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env vpython3
2# Copyright 2020 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import copy
7import json
8import subprocess
9import sys
10from typing import List, Tuple
11import unittest
12
13import unittest.mock as mock
14
15from unexpected_passes_common import builders
16from unexpected_passes_common import constants
17from unexpected_passes_common import data_types
18from unexpected_passes_common import expectations
19from unexpected_passes_common import multiprocessing_utils
20from unexpected_passes_common import queries
21from unexpected_passes_common import unittest_utils
22
23queries.QUERY_DELAY = 0
24
25
26class HelperMethodUnittest(unittest.TestCase):
27  def testStripPrefixFromBuildIdValidId(self) -> None:
28    self.assertEqual(queries._StripPrefixFromBuildId('build-1'), '1')
29
30  def testStripPrefixFromBuildIdInvalidId(self) -> None:
31    with self.assertRaises(AssertionError):
32      queries._StripPrefixFromBuildId('build1')
33    with self.assertRaises(AssertionError):
34      queries._StripPrefixFromBuildId('build-1-2')
35
36  def testConvertActualResultToExpectationFileFormatAbort(self) -> None:
37    self.assertEqual(
38        queries._ConvertActualResultToExpectationFileFormat('ABORT'), 'Timeout')
39
40
41class QueryGeneratorUnittest(unittest.TestCase):
42  def setUp(self):
43    self._builder = data_types.BuilderEntry('ci', constants.BuilderTypes.CI,
44                                            False)
45
46  def testSplitQueryGeneratorInitialSplit(self) -> None:
47    """Tests that initial query splitting works as expected."""
48    test_filter = queries.SplitQueryGenerator(self._builder, ['1', '2', '3'], 2)
49    self.assertEqual(test_filter._test_id_lists, [['1', '2'], ['3']])
50    self.assertEqual(len(test_filter.GetClauses()), 2)
51    test_filter = queries.SplitQueryGenerator(self._builder, ['1', '2', '3'], 3)
52    self.assertEqual(test_filter._test_id_lists, [['1', '2', '3']])
53    self.assertEqual(len(test_filter.GetClauses()), 1)
54
55  def testSplitQueryGeneratorSplitQuery(self) -> None:
56    """Tests that SplitQueryGenerator's query splitting works."""
57    test_filter = queries.SplitQueryGenerator(self._builder, ['1', '2'], 10)
58    self.assertEqual(len(test_filter.GetClauses()), 1)
59    test_filter.SplitQuery()
60    self.assertEqual(len(test_filter.GetClauses()), 2)
61
62  def testSplitQueryGeneratorSplitQueryCannotSplitFurther(self) -> None:
63    """Tests that SplitQueryGenerator's failure mode."""
64    test_filter = queries.SplitQueryGenerator(self._builder, ['1'], 1)
65    with self.assertRaises(queries.QuerySplitError):
66      test_filter.SplitQuery()
67
68
69class QueryBuilderUnittest(unittest.TestCase):
70  def setUp(self) -> None:
71    self._patcher = mock.patch.object(subprocess, 'Popen')
72    self._popen_mock = self._patcher.start()
73    self.addCleanup(self._patcher.stop)
74
75    builders.ClearInstance()
76    expectations.ClearInstance()
77    unittest_utils.RegisterGenericBuildersImplementation()
78    unittest_utils.RegisterGenericExpectationsImplementation()
79    self._querier = unittest_utils.CreateGenericQuerier()
80
81    self._relevant_file_patcher = mock.patch.object(
82        self._querier,
83        '_GetRelevantExpectationFilesForQueryResult',
84        return_value=None)
85    self._relevant_file_mock = self._relevant_file_patcher.start()
86    self.addCleanup(self._relevant_file_patcher.stop)
87
88    self._builder = data_types.BuilderEntry('builder',
89                                            constants.BuilderTypes.CI, False)
90
91  def testQueryFailureRaised(self) -> None:
92    """Tests that a query failure is properly surfaced."""
93    self._popen_mock.return_value = unittest_utils.FakeProcess(returncode=1)
94    with self.assertRaises(RuntimeError):
95      self._querier.QueryBuilder(
96          data_types.BuilderEntry('builder', constants.BuilderTypes.CI, False))
97
98  def testInvalidNumSamples(self) -> None:
99    """Tests that the number of samples is validated."""
100    with self.assertRaises(AssertionError):
101      unittest_utils.CreateGenericQuerier(num_samples=-1)
102
103  def testInvalidNumJobs(self) -> None:
104    """Tests that the number of jobs is validated."""
105    with self.assertRaises(AssertionError):
106      unittest_utils.CreateGenericQuerier(num_jobs=0)
107
108  def testNoResults(self) -> None:
109    """Tests functionality if the query returns no results."""
110    self._popen_mock.return_value = unittest_utils.FakeProcess(stdout='[]')
111    results, expectation_files = self._querier.QueryBuilder(
112        data_types.BuilderEntry('builder', constants.BuilderTypes.CI, False))
113    self.assertEqual(results, [])
114    self.assertIsNone(expectation_files, None)
115
116  def testValidResults(self) -> None:
117    """Tests functionality when valid results are returned."""
118    self._relevant_file_mock.return_value = ['foo_expectations']
119    query_results = [
120        {
121            'id':
122            'build-1234',
123            'test_id': ('ninja://chrome/test:telemetry_gpu_integration_test/'
124                        'gpu_tests.pixel_integration_test.'
125                        'PixelIntegrationTest.test_name'),
126            'status':
127            'FAIL',
128            'typ_expectations': [
129                'RetryOnFailure',
130            ],
131            'typ_tags': [
132                'win',
133                'intel',
134                'unknown_tag',  # This is expected to be removed.
135            ],
136            'step_name':
137            'step_name',
138        },
139    ]
140    self._popen_mock.return_value = unittest_utils.FakeProcess(
141        stdout=json.dumps(query_results))
142    results, expectation_files = self._querier.QueryBuilder(
143        data_types.BuilderEntry('builder', constants.BuilderTypes.CI, False))
144    self.assertEqual(len(results), 1)
145    self.assertEqual(
146        results[0],
147        data_types.Result('test_name', ['win', 'intel'], 'Failure', 'step_name',
148                          '1234'))
149    self.assertEqual(expectation_files, ['foo_expectations'])
150
151  def testValidResultsNoneExpectations(self) -> None:
152    """Tests when an implementation uses None for expectation files."""
153    query_results = [
154        {
155            'id':
156            'build-1234',
157            'test_id': ('ninja://chrome/test:telemetry_gpu_integration_test/'
158                        'gpu_tests.pixel_integration_test.'
159                        'PixelIntegrationTest.test_name'),
160            'status':
161            'FAIL',
162            'typ_expectations': [
163                'RetryOnFailure',
164            ],
165            'typ_tags': [
166                'win',
167                'intel',
168            ],
169            'step_name':
170            'step_name',
171        },
172        {
173            'id':
174            'build-1234',
175            'test_id': ('ninja://chrome/test:telemetry_gpu_integration_test/'
176                        'gpu_tests.pixel_integration_test.'
177                        'PixelIntegrationTest.test_name'),
178            'status':
179            'FAIL',
180            'typ_expectations': [
181                'RetryOnFailure',
182            ],
183            'typ_tags': [
184                'win',
185                'nvidia',
186            ],
187            'step_name':
188            'step_name',
189        },
190    ]
191    self._popen_mock.return_value = unittest_utils.FakeProcess(
192        stdout=json.dumps(query_results))
193    with mock.patch.object(
194        self._querier, '_GetRelevantExpectationFilesForQueryResult') as ef_mock:
195      ef_mock.return_value = None
196      results, expectation_files = self._querier.QueryBuilder(
197          data_types.BuilderEntry('builder', constants.BuilderTypes.CI, False))
198      self.assertEqual(len(results), 2)
199      self.assertIn(
200          data_types.Result('test_name', ['win', 'intel'], 'Failure',
201                            'step_name', '1234'), results)
202      self.assertIn(
203          data_types.Result('test_name', ['win', 'nvidia'], 'Failure',
204                            'step_name', '1234'), results)
205      self.assertIsNone(expectation_files)
206      ef_mock.assert_called_once()
207
208  def testValidResultsMultipleSteps(self) -> None:
209    """Tests functionality when results from multiple steps are present."""
210
211    def SideEffect(result: queries.QueryResult) -> List[str]:
212      if result['step_name'] == 'a step name':
213        return ['foo_expectations']
214      if result['step_name'] == 'another step name':
215        return ['bar_expectations']
216      raise RuntimeError('Unknown step %s' % result['step_name'])
217
218    self._relevant_file_mock.side_effect = SideEffect
219    query_results = [
220        {
221            'id': 'build-1234',
222            'test_id': 'ninja://:blink_web_tests/some/test/with.test_name',
223            'status': 'FAIL',
224            'typ_expectations': [
225                'Failure',
226            ],
227            'typ_tags': [
228                'linux',
229                'intel',
230            ],
231            'step_name': 'a step name',
232        },
233        {
234            'id': 'build-1234',
235            'test_id': 'ninja://:blink_web_tests/some/test/with.test_name',
236            'status': 'FAIL',
237            'typ_expectations': [
238                'Crash',
239            ],
240            'typ_tags': [
241                'linux',
242                'amd',
243            ],
244            'step_name': 'another step name',
245        },
246    ]
247    self._popen_mock.return_value = unittest_utils.FakeProcess(
248        stdout=json.dumps(query_results))
249    results, expectation_files = self._querier.QueryBuilder(
250        data_types.BuilderEntry('builder', constants.BuilderTypes.CI, False))
251    self.assertEqual(len(results), 2)
252    self.assertIn(
253        data_types.Result('test_name', ['linux', 'intel'], 'Failure',
254                          'a step name', '1234'), results)
255    self.assertIn(
256        data_types.Result('test_name', ['linux', 'amd'], 'Failure',
257                          'another step name', '1234'), results)
258    self.assertEqual(len(expectation_files), 2)
259    self.assertEqual(set(expectation_files),
260                     set(['foo_expectations', 'bar_expectations']))
261
262  def testFilterInsertion(self) -> None:
263    """Tests that test filters are properly inserted into the query."""
264    with mock.patch.object(
265        self._querier,
266        '_GetQueryGeneratorForBuilder',
267        return_value=unittest_utils.SimpleFixedQueryGenerator(
268            self._builder, 'a real filter')), mock.patch.object(
269                self._querier,
270                '_RunBigQueryCommandsForJsonOutput') as query_mock:
271      self._querier.QueryBuilder(self._builder)
272      query_mock.assert_called_once()
273      query = query_mock.call_args[0][0][0]
274      self.assertIn('a real filter', query)
275
276  def testEarlyReturnOnNoFilter(self) -> None:
277    """Tests that the absence of a test filter results in an early return."""
278    with mock.patch.object(
279        self._querier, '_GetQueryGeneratorForBuilder',
280        return_value=None), mock.patch.object(
281            self._querier, '_RunBigQueryCommandsForJsonOutput') as query_mock:
282      results, expectation_files = self._querier.QueryBuilder(self._builder)
283      query_mock.assert_not_called()
284      self.assertEqual(results, [])
285      self.assertEqual(expectation_files, None)
286
287  def testRetryOnMemoryLimit(self) -> None:
288    """Tests that queries are split and retried if the memory limit is hit."""
289
290    def SideEffect(*_, **__) -> list:
291      SideEffect.call_count += 1
292      if SideEffect.call_count == 1:
293        raise queries.MemoryLimitError()
294      return []
295
296    SideEffect.call_count = 0
297
298    with mock.patch.object(
299        self._querier,
300        '_GetQueryGeneratorForBuilder',
301        return_value=unittest_utils.SimpleSplitQueryGenerator(
302            self._builder, ['filter_a', 'filter_b'], 10)), mock.patch.object(
303                self._querier,
304                '_RunBigQueryCommandsForJsonOutput') as query_mock:
305      query_mock.side_effect = SideEffect
306      self._querier.QueryBuilder(self._builder)
307      self.assertEqual(query_mock.call_count, 2)
308
309      args, _ = unittest_utils.GetArgsForMockCall(query_mock.call_args_list, 0)
310      first_query = args[0][0]
311      self.assertIn('filter_a', first_query)
312      self.assertIn('filter_b', first_query)
313
314      args, _ = unittest_utils.GetArgsForMockCall(query_mock.call_args_list, 1)
315      second_query_first_half = args[0][0]
316      self.assertIn('filter_a', second_query_first_half)
317      self.assertNotIn('filter_b', second_query_first_half)
318
319      second_query_second_half = args[0][1]
320      self.assertIn('filter_b', second_query_second_half)
321      self.assertNotIn('filter_a', second_query_second_half)
322
323
324class FillExpectationMapForBuildersUnittest(unittest.TestCase):
325  def setUp(self) -> None:
326    self._querier = unittest_utils.CreateGenericQuerier()
327
328    self._query_patcher = mock.patch.object(self._querier, 'QueryBuilder')
329    self._query_mock = self._query_patcher.start()
330    self.addCleanup(self._query_patcher.stop)
331    self._pool_patcher = mock.patch.object(multiprocessing_utils,
332                                           'GetProcessPool')
333    self._pool_mock = self._pool_patcher.start()
334    self._pool_mock.return_value = unittest_utils.FakePool()
335    self.addCleanup(self._pool_patcher.stop)
336    self._filter_patcher = mock.patch.object(self._querier,
337                                             '_FilterOutInactiveBuilders')
338    self._filter_mock = self._filter_patcher.start()
339    self._filter_mock.side_effect = lambda b, _: b
340    self.addCleanup(self._filter_patcher.stop)
341
342  def testErrorOnMixedBuilders(self) -> None:
343    """Tests that providing builders of mixed type is an error."""
344    builders_to_fill = [
345        data_types.BuilderEntry('ci_builder', constants.BuilderTypes.CI, False),
346        data_types.BuilderEntry('try_builder', constants.BuilderTypes.TRY,
347                                False)
348    ]
349    with self.assertRaises(AssertionError):
350      self._querier.FillExpectationMapForBuilders(
351          data_types.TestExpectationMap({}), builders_to_fill)
352
353  def testValidResults(self) -> None:
354    """Tests functionality when valid results are returned by the query."""
355
356    def SideEffect(builder: data_types.BuilderEntry,
357                   *args) -> Tuple[data_types.ResultListType, None]:
358      del args
359      if builder.name == 'matched_builder':
360        return ([
361            data_types.Result('foo', ['win'], 'Pass', 'step_name', 'build_id')
362        ], None)
363      if builder.name == 'matched_internal':
364        return ([
365            data_types.Result('foo', ['win'], 'Pass', 'step_name_internal',
366                              'build_id')
367        ], None)
368      if builder.name == 'unmatched_internal':
369        return ([
370            data_types.Result('bar', [], 'Pass', 'step_name_internal',
371                              'build_id')
372        ], None)
373      return ([data_types.Result('bar', [], 'Pass', 'step_name',
374                                 'build_id')], None)
375
376    self._query_mock.side_effect = SideEffect
377
378    expectation = data_types.Expectation('foo', ['win'], 'RetryOnFailure')
379    expectation_map = data_types.TestExpectationMap({
380        'foo':
381        data_types.ExpectationBuilderMap({
382            expectation:
383            data_types.BuilderStepMap(),
384        }),
385    })
386    builders_to_fill = [
387        data_types.BuilderEntry('matched_builder', constants.BuilderTypes.CI,
388                                False),
389        data_types.BuilderEntry('unmatched_builder', constants.BuilderTypes.CI,
390                                False),
391        data_types.BuilderEntry('matched_internal', constants.BuilderTypes.CI,
392                                True),
393        data_types.BuilderEntry('unmatched_internal', constants.BuilderTypes.CI,
394                                True),
395    ]
396    unmatched_results = self._querier.FillExpectationMapForBuilders(
397        expectation_map, builders_to_fill)
398    stats = data_types.BuildStats()
399    stats.AddPassedBuild(frozenset(['win']))
400    expected_expectation_map = {
401        'foo': {
402            expectation: {
403                'chromium/ci:matched_builder': {
404                    'step_name': stats,
405                },
406                'chrome/ci:matched_internal': {
407                    'step_name_internal': stats,
408                },
409            },
410        },
411    }
412    self.assertEqual(expectation_map, expected_expectation_map)
413    self.assertEqual(
414        unmatched_results, {
415            'chromium/ci:unmatched_builder': [
416                data_types.Result('bar', [], 'Pass', 'step_name', 'build_id'),
417            ],
418            'chrome/ci:unmatched_internal': [
419                data_types.Result('bar', [], 'Pass', 'step_name_internal',
420                                  'build_id'),
421            ],
422        })
423
424  def testQueryFailureIsSurfaced(self) -> None:
425    """Tests that a query failure is properly surfaced despite being async."""
426    self._query_mock.side_effect = IndexError('failure')
427    with self.assertRaises(IndexError):
428      self._querier.FillExpectationMapForBuilders(
429          data_types.TestExpectationMap(), [
430              data_types.BuilderEntry('matched_builder',
431                                      constants.BuilderTypes.CI, False)
432          ])
433
434
435class FilterOutInactiveBuildersUnittest(unittest.TestCase):
436  def setUp(self) -> None:
437    self._subprocess_patcher = mock.patch(
438        'unexpected_passes_common.queries.subprocess.Popen')
439    self._subprocess_mock = self._subprocess_patcher.start()
440    self.addCleanup(self._subprocess_patcher.stop)
441
442    self._querier = unittest_utils.CreateGenericQuerier()
443
444  def testAllActiveBuilders(self) -> None:
445    """Tests that no builders are removed if no inactive builders are found."""
446    results = [{
447        'builder_name': 'foo_builder',
448    }, {
449        'builder_name': 'bar_builder',
450    }]
451    fake_process = unittest_utils.FakeProcess(stdout=json.dumps(results))
452    self._subprocess_mock.return_value = fake_process
453    initial_builders = [
454        data_types.BuilderEntry('foo_builder', constants.BuilderTypes.CI,
455                                False),
456        data_types.BuilderEntry('bar_builder', constants.BuilderTypes.CI,
457                                False),
458    ]
459    expected_builders = copy.copy(initial_builders)
460    filtered_builders = self._querier._FilterOutInactiveBuilders(
461        initial_builders, constants.BuilderTypes.CI)
462    self.assertEqual(filtered_builders, expected_builders)
463
464  def testInactiveBuilders(self) -> None:
465    """Tests that inactive builders are removed."""
466    results = [{
467        'builder_name': 'foo_builder',
468    }]
469    fake_process = unittest_utils.FakeProcess(stdout=json.dumps(results))
470    self._subprocess_mock.return_value = fake_process
471    initial_builders = [
472        data_types.BuilderEntry('foo_builder', constants.BuilderTypes.CI,
473                                False),
474        data_types.BuilderEntry('bar_builder', constants.BuilderTypes.CI,
475                                False),
476    ]
477    expected_builders = [
478        data_types.BuilderEntry('foo_builder', constants.BuilderTypes.CI, False)
479    ]
480    filtered_builders = self._querier._FilterOutInactiveBuilders(
481        initial_builders, constants.BuilderTypes.CI)
482    self.assertEqual(filtered_builders, expected_builders)
483
484  def testByteConversion(self) -> None:
485    """Tests that bytes are properly handled if returned."""
486    results = [{
487        'builder_name': 'foo_builder',
488    }]
489    fake_process = unittest_utils.FakeProcess(stdout=json.dumps(results))
490    self._subprocess_mock.return_value = fake_process
491    initial_builders = [
492        data_types.BuilderEntry('foo_builder', constants.BuilderTypes.CI,
493                                False),
494        data_types.BuilderEntry('bar_builder', constants.BuilderTypes.CI,
495                                False),
496    ]
497    expected_builders = [
498        data_types.BuilderEntry('foo_builder', constants.BuilderTypes.CI, False)
499    ]
500    filtered_builders = self._querier._FilterOutInactiveBuilders(
501        initial_builders, constants.BuilderTypes.CI)
502    self.assertEqual(filtered_builders, expected_builders)
503
504
505class RunBigQueryCommandsForJsonOutputUnittest(unittest.TestCase):
506  def setUp(self) -> None:
507    self._popen_patcher = mock.patch.object(subprocess, 'Popen')
508    self._popen_mock = self._popen_patcher.start()
509    self.addCleanup(self._popen_patcher.stop)
510
511    self._querier = unittest_utils.CreateGenericQuerier()
512
513  def testJsonReturned(self) -> None:
514    """Tests that valid JSON parsed from stdout is returned."""
515    query_output = [{'foo': 'bar'}]
516    self._popen_mock.return_value = unittest_utils.FakeProcess(
517        stdout=json.dumps(query_output))
518    result = self._querier._RunBigQueryCommandsForJsonOutput([''], {})
519    self.assertEqual(result, query_output)
520    self._popen_mock.assert_called_once()
521
522  def testJsonReturnedSplitQuery(self) -> None:
523    """Tests that valid JSON is returned when a split query is used."""
524
525    def SideEffect(*_, **__) -> unittest_utils.FakeProcess:
526      SideEffect.call_count += 1
527      if SideEffect.call_count == 1:
528        return unittest_utils.FakeProcess(stdout=json.dumps([{'foo': 'bar'}]))
529      return unittest_utils.FakeProcess(stdout=json.dumps([{'bar': 'baz'}]))
530
531    SideEffect.call_count = 0
532
533    self._popen_mock.side_effect = SideEffect
534    result = self._querier._RunBigQueryCommandsForJsonOutput(['1', '2'], {})
535
536    self.assertEqual(len(result), 2)
537    self.assertIn({'foo': 'bar'}, result)
538    self.assertIn({'bar': 'baz'}, result)
539
540  def testExceptionRaisedOnFailure(self) -> None:
541    """Tests that an exception is raised if the query fails."""
542    self._popen_mock.return_value = unittest_utils.FakeProcess(returncode=1)
543    with self.assertRaises(RuntimeError):
544      self._querier._RunBigQueryCommandsForJsonOutput([''], {})
545
546  def testRateLimitRetrySuccess(self) -> None:
547    """Tests that rate limit errors result in retries."""
548
549    def SideEffect(*_, **__) -> unittest_utils.FakeProcess:
550      SideEffect.call_count += 1
551      if SideEffect.call_count == 1:
552        return unittest_utils.FakeProcess(
553            returncode=1, stdout='Exceeded rate limits for foo.')
554      return unittest_utils.FakeProcess(stdout='[]')
555
556    SideEffect.call_count = 0
557
558    self._popen_mock.side_effect = SideEffect
559    self._querier._RunBigQueryCommandsForJsonOutput([''], {})
560    self.assertEqual(self._popen_mock.call_count, 2)
561
562  def testRateLimitRetryFailure(self) -> None:
563    """Tests that rate limit errors stop retrying after enough iterations."""
564    self._popen_mock.return_value = unittest_utils.FakeProcess(
565        returncode=1, stdout='Exceeded rate limits for foo.')
566    with self.assertRaises(RuntimeError):
567      self._querier._RunBigQueryCommandsForJsonOutput([''], {})
568    self.assertEqual(self._popen_mock.call_count, queries.MAX_QUERY_TRIES)
569
570  def testBatching(self) -> None:
571    """Tests that batching preferences are properly forwarded."""
572    query_output = [{'foo': 'bar'}]
573    self._popen_mock.return_value = unittest_utils.FakeProcess(
574        stdout=json.dumps(query_output))
575
576    self._querier._RunBigQueryCommandsForJsonOutput([''], {})
577    self._popen_mock.assert_called_once()
578    args, _ = unittest_utils.GetArgsForMockCall(self._popen_mock.call_args_list,
579                                                0)
580    cmd = args[0]
581    self.assertIn('--batch', cmd)
582
583    self._querier = unittest_utils.CreateGenericQuerier(use_batching=False)
584    self._popen_mock.reset_mock()
585    self._querier._RunBigQueryCommandsForJsonOutput([''], {})
586    self._popen_mock.assert_called_once()
587    args, _ = unittest_utils.GetArgsForMockCall(self._popen_mock.call_args_list,
588                                                0)
589    cmd = args[0]
590    self.assertNotIn('--batch', cmd)
591
592
593class GenerateBigQueryCommandUnittest(unittest.TestCase):
594  def testNoParametersSpecified(self) -> None:
595    """Tests that no parameters are added if none are specified."""
596    cmd = queries.GenerateBigQueryCommand('project', {})
597    for element in cmd:
598      self.assertFalse(element.startswith('--parameter'))
599
600  def testParameterAddition(self) -> None:
601    """Tests that specified parameters are added appropriately."""
602    cmd = queries.GenerateBigQueryCommand('project', {
603        '': {
604            'string': 'string_value'
605        },
606        'INT64': {
607            'int': 1
608        }
609    })
610    self.assertIn('--parameter=string::string_value', cmd)
611    self.assertIn('--parameter=int:INT64:1', cmd)
612
613  def testBatchMode(self) -> None:
614    """Tests that batch mode adds the necessary arg."""
615    cmd = queries.GenerateBigQueryCommand('project', {}, batch=True)
616    self.assertIn('--batch', cmd)
617
618
619if __name__ == '__main__':
620  unittest.main(verbosity=2)
621