Coverage for CIResults/tests/test_filtering.py: 100%
565 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-19 09:20 +0000
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-19 09:20 +0000
1import datetime
2from unittest.mock import patch
4import pytz
5from django.core.management import call_command
6from django.db import models
7from django.db.models import Q
8from django.test import TestCase
9from django.test.client import RequestFactory
10from model_bakery import baker
12from CIResults.filtering import (
13 FilterObject,
14 FilterObjectBool,
15 FilterObjectDateTime,
16 FilterObjectDuration,
17 FilterObjectInteger,
18 FilterObjectJSON,
19 FilterObjectModel,
20 FilterObjectStr,
21 LegacyParser,
22 QueryCreator,
23 QueryParser,
24 QueryParserPython,
25 UserFiltrableMixin,
26 VisitorQ,
27)
28from CIResults.models import (
29 Bug,
30 Issue,
31 KnownFailure,
32 Machine,
33 MachineTag,
34 RunConfigTag,
35 TestResult,
36 TestSuite,
37 TestsuiteRun,
38)
39from shortener.models import Shortener
42class UserFiltrableTestsMixin:
43 def test_filter_objects_to_db(self):
44 # Abort if the class does not have a model
45 if not hasattr(self, 'Model'):
46 raise ValueError("The class '{}' does not have a 'Model' attribute".format(self)) # pragma: no cover
48 # Abort if the object does not have the filter_objects_to_db attribute
49 if not hasattr(self.Model, 'filter_objects_to_db'):
50 raise ValueError("The model '{}' does not have a 'filter_objects_to_db "
51 "attribute'".format(self.Model)) # pragma: no cover
53 # execute the query with USE_TZ=False to ignore the naive datetime warning
54 with self.settings(USE_TZ=False):
55 for field_name, db_obj in self.Model.filter_objects_to_db.items():
56 if isinstance(db_obj, FilterObjectModel):
57 filter_name = '{}__in'.format(db_obj.db_path)
58 value = db_obj.model.objects.none()
59 elif isinstance(db_obj, FilterObjectJSON):
60 db_obj.key = 'key'
61 filter_name = '{}__exact'.format(db_obj.db_path)
62 value = db_obj.test_value
63 else:
64 filter_name = '{}__exact'.format(db_obj.db_path)
65 value = db_obj.test_value
67 try:
68 self.Model.objects.filter(**{filter_name: value})
69 except Exception as e: # pragma: no cover
70 self.fail("Class {}'s field '{}' is not working: {}.".format(self.Model,
71 field_name,
72 str(e))) # pragma: no cover
75class BugTests(TestCase, UserFiltrableTestsMixin):
76 Model = Bug
79class IssueTests(TestCase, UserFiltrableTestsMixin):
80 Model = Issue
83class TestsuiteRunTests(TestCase, UserFiltrableTestsMixin):
84 Model = TestsuiteRun
87class TestResultTests(TestCase, UserFiltrableTestsMixin):
88 Model = TestResult
91class KnownFailureTests(TestCase, UserFiltrableTestsMixin):
92 Model = KnownFailure
95class QueryVisitorTests(TestCase):
96 def test_get_related_model(self):
97 queryVisitor = VisitorQ(KnownFailure)
98 self.assertEqual(queryVisitor.get_related_model("result"), TestResult)
100 def test_get_related_model_no_attribute(self):
101 queryVisitor = VisitorQ(KnownFailure)
102 with self.assertRaisesMessage(AttributeError, "'KnownFailure' has no attribute 'status'"):
103 queryVisitor.get_related_model("status")
106class TestModelChild(models.Model):
107 number = models.IntegerField()
108 string = models.CharField(max_length=100)
109 filter_objects_to_db = {
110 "number": FilterObjectInteger('number'),
111 "string": FilterObjectStr('string'),
112 }
115class TestModel(models.Model):
116 number = models.IntegerField()
117 string = models.CharField(max_length=100)
118 date = models.DateTimeField()
119 duration = models.DurationField()
120 boolean = models.BooleanField()
121 json = models.JSONField()
122 child = models.ForeignKey(TestModelChild, on_delete=models.CASCADE)
124 filter_objects_to_db = {
125 "number": FilterObjectInteger('number'),
126 "string": FilterObjectStr('string'),
127 "date": FilterObjectDateTime('date'),
128 "duration": FilterObjectDuration('duration'),
129 "boolean": FilterObjectBool('boolean'),
130 "json": FilterObjectJSON('json'),
131 "child": FilterObjectModel(TestModelChild, 'child'),
132 }
135call_command('makemigrations', 'CIResults')
138class QueryParserTests(TestCase):
139 def test_empty_query(self):
140 parser = QueryParser(TestResult, "")
141 self.assertTrue(parser.is_valid, parser.error)
142 self.assertTrue(parser.is_empty)
143 self.assertEqual(parser.q_objects, Q())
144 self.assertEqual(parser.error, None)
146 def test_unknown_object_name(self):
147 parser = QueryParser(TestResult, "hello = 'world'")
149 self.assertFalse(parser.is_valid, parser.error)
150 self.assertTrue(parser.is_empty)
151 self.assertEqual(parser.q_objects, Q())
152 self.assertEqual(parser.error, "The object 'hello' does not exist")
154 def test_key_with_double_underscore(self):
155 parser = QueryParser(TestResult, "json.toto__tata = 'world'")
157 self.assertFalse(parser.is_valid, parser.error)
158 self.assertTrue(parser.is_empty)
159 self.assertEqual(parser.q_objects, Q())
160 self.assertEqual(parser.error, "Dict object keys cannot contain the substring '__'")
162 def test_two_keys_on_keyed_object(self):
163 parser = QueryParser(TestModel, "json.toto.tata = 'world'")
165 self.assertFalse(parser.is_valid, parser.error)
166 self.assertTrue(parser.is_empty)
167 self.assertEqual(parser.q_objects, Q())
169 def test_no_key_on_keyed_object(self):
170 parser = QueryParser(TestModel, "json = 'world'")
172 self.assertFalse(parser.is_valid, parser.error)
173 self.assertTrue(parser.is_empty)
174 self.assertEqual(parser.q_objects, Q())
175 self.assertEqual(parser.error, "The dict object 'json' requires a key to access its data")
177 def test_key_on_non_keyed_object(self):
178 parser = QueryParser(TestModel, "date.toto = 'world'")
180 self.assertFalse(parser.is_valid, parser.error)
181 self.assertTrue(parser.is_empty)
182 self.assertEqual(parser.q_objects, Q())
183 self.assertEqual(parser.error, "The object 'date' cannot have an associated key")
185 def test_invalid_syntax(self):
186 parser = QueryParser(TestModel, "hello = 'world")
188 self.assertFalse(parser.is_valid, parser.error)
189 self.assertTrue(parser.is_empty)
190 self.assertEqual(parser.q_objects, Q())
191 self.assertEqual(parser.error, "Expected ''' at position (1, 15) => 'o = 'world*'.")
193 @patch('django.utils.timezone.now',
194 return_value=datetime.datetime.strptime('2019-01-01T00:00:05', "%Y-%m-%dT%H:%M:%S"))
195 def test_parsing_all_types(self, now_mocked):
196 parser = QueryParser(TestModel, "number=123 AND string = 'HELLO' AND date=datetime(2019-02-01) "
197 "AND duration = duration(00:00:03) AND duration > ago(00:00:05) "
198 "AND boolean = TRUE AND json.foo_bar = 'bar'"
199 "AND json.foo_bar2 = 42")
201 self.assertTrue(parser.is_valid, parser.error)
202 self.assertFalse(parser.is_empty)
203 # HACK: Using 'children' attribute and set() here because the ordering was different, for some reason, between
204 # Q objects, which caused direct comparison to fail.
205 self.assertEqual(
206 set(parser.q_objects.children),
207 set(
208 Q(
209 number__exact=123,
210 string__exact="HELLO",
211 boolean__exact=True,
212 date__exact=FilterObjectDateTime.parse_value("2019-02-01"),
213 duration__exact=datetime.timedelta(seconds=3),
214 duration__gt=datetime.datetime.strptime("2019-01-01", "%Y-%m-%d"),
215 json__foo_bar__exact="bar",
216 json__foo_bar2__exact=42,
217 ).children
218 ),
219 )
221 def test_integer_lookups(self):
222 for lookup, suffix in [("<=", "lte"), (">=", "gte"), ("<", "lt"), (">", "gt"), ("<", "lt"), ("=", "exact")]:
223 parser = QueryParser(TestModel, f"number {lookup} 1234")
224 key = f"number__{suffix}"
225 self.assertEqual(parser.q_objects, Q(**{key: 1234}))
227 parser = QueryParser(TestModel, "number IS IN [12, 34]")
229 self.assertTrue(parser.is_valid, parser.error)
230 self.assertFalse(parser.is_empty)
231 self.assertEqual(parser.q_objects, Q(number__in=[12, 34]))
233 def test_string_lookups(self):
234 for lookup, suffix, negated in [
235 ("CONTAINS", "contains", False),
236 ("ICONTAINS", "icontains", False),
237 ("MATCHES", "regex", False),
238 ("~=", "regex", False),
239 ("=", "exact", False),
240 ("!=", "exact", True),
241 ]:
242 parser = QueryParser(TestModel, f"string {lookup} 'hello'")
244 key = f"string__{suffix}"
245 expected = Q(**{key: "hello"})
247 if negated:
248 self.assertEqual(parser.q_objects, ~expected)
249 else:
250 self.assertEqual(parser.q_objects, expected)
252 parser = QueryParser(TestModel, "string IS IN ['hello','world']")
253 self.assertEqual(parser.q_objects, Q(string__in=['hello', 'world']))
255 def test_empty_string_query(self):
256 parser = QueryParser(TestModel, "string = ''")
257 key = "string__exact"
258 expected = Q(**{key: ""})
259 self.assertEqual(parser.q_objects, expected)
261 def test_escaped_string_query(self):
262 for quote in ["'", '"']:
263 query = f"string = {quote}foo\\{quote}bar{quote}"
264 parser = QueryParser(TestModel, query)
265 self.assertTrue(parser.is_valid)
266 self.assertEqual(parser.error, None)
267 expected = Q(**{"string__exact": f"foo\\{quote}bar"})
268 self.assertEqual(parser.q_objects, expected)
270 def test_limit_alone(self):
271 parser = QueryParser(TestModel, "number=123 AND string = 'HELLO' LIMIT 42")
272 self.assertEqual(parser.limit, 42)
274 def test_limit_negative(self):
275 parser = QueryParser(TestModel, "number=123 AND string = 'HELLO' LIMIT -42")
277 self.assertFalse(parser.is_valid, )
278 self.assertEqual(parser.error, "Negative limits are not supported")
280 def test_orderby_alone(self):
281 parser = QueryParser(TestModel, "number=123 AND string = 'HELLO' ORDER_BY -string")
282 self.assertEqual(parser.orderby, "-string")
284 def test_orderby_invalid_object(self):
285 parser = QueryParser(TestModel, "number=123 AND string = 'HELLO' ORDER_BY toto")
287 self.assertFalse(parser.is_valid, )
288 self.assertEqual(parser.error, "The object 'toto' does not exist")
290 def test_orderby_limit_interaction(self):
291 parser = QueryParser(TestModel, "number=123 AND string = 'HELLO' ORDER_BY string LIMIT 42")
293 self.assertTrue(parser.is_valid, parser.error)
294 self.assertFalse(parser.is_empty)
295 self.assertEqual(parser.q_objects,
296 Q(number__exact=123, string__exact='HELLO'))
297 self.assertEqual(parser.limit, 42)
298 self.assertEqual(parser.orderby, "string")
300 def test_invalid_subquery(self):
301 parser = QueryParser(TestModel, "number=123 AND child MATCHES (not_existient_field=123) AND string = 'TOTO'")
303 self.assertFalse(parser.is_valid)
304 self.assertEqual(parser.error, "The object 'not_existient_field' does not exist")
305 self.assertTrue(parser.is_empty)
307 def test_subquery(self):
308 test_result = baker.make(TestResult, ts_run__runconfig__name="run_1")
309 parser = QueryParser(TestResult, "runconfig MATCHES (name='run_1')")
311 self.assertTrue(parser.is_valid, parser.error)
312 self.assertFalse(parser.is_empty)
314 self.assertIn(test_result, parser.objects)
316 def test_complex_query1(self):
317 parser = QueryParser(TestModel, '''(string IS IN ["toto","titi"] AND date=datetime(2018-06-23)) OR
318 ((number > 456 AND NOT string ~= "hello" ) OR number < 456)''')
319 q_filter = (Q(**{'string__in': ['toto', 'titi']}) & Q(**{'date__exact':
320 datetime.datetime(2018, 6, 23, 0, 0, tzinfo=pytz.utc)})) | ((Q(**{'number__gt': 456})
321 & ~Q(**{'string__regex': 'hello'})) |
322 Q(**{'number__lt': 456}))
323 self.assertTrue(parser.is_valid, parser.error)
324 self.assertFalse(parser.is_empty)
325 self.assertEqual(parser.q_objects, q_filter)
327 def test_complex_query2(self):
328 parser = QueryParser(TestModel, '''(number IS IN [2,3,4] OR number NOT IN [2,3] )
329 AND (number <= 1 OR number >= 0)''')
330 q_filter = (Q(**{'number__in': [2, 3, 4]}) | ~Q(**{'number__in': [2, 3]}))\
331 & (Q(**{'number__lte': 1}) | Q(**{'number__gte': 0}))
333 self.assertTrue(parser.is_valid, parser.error)
334 self.assertFalse(parser.is_empty)
335 self.assertEqual(parser.q_objects, q_filter)
337 def test_ignore_fields__all_fields_ignored(self):
338 parser = QueryParser(TestResult, "status_name = 'fail'", ignore_fields=["status_name"])
339 self.assertEqual(
340 ('SELECT DISTINCT "CIResults_testresult"."id", "CIResults_testresult"."test_id", '
341 '"CIResults_testresult"."ts_run_id", "CIResults_testresult"."status_id", "CIResults_testresult"."url", '
342 '"CIResults_testresult"."start", "CIResults_testresult"."duration", "CIResults_testresult"."command", '
343 '"CIResults_testresult"."stdout", "CIResults_testresult"."stderr", "CIResults_testresult"."dmesg" FROM '
344 '"CIResults_testresult"'),
345 str(parser.objects.query)
346 )
348 def test_ignore_fields__complex_query_with_multiple_ignored_fields(self):
349 parser = QueryParser(TestResult, "status_name = 'fail' AND (stdout = 'out' OR stderr = 'err')",
350 ignore_fields=["status_name", "stderr"])
351 self.assertEqual(
352 ('SELECT DISTINCT "CIResults_testresult"."id", "CIResults_testresult"."test_id", '
353 '"CIResults_testresult"."ts_run_id", "CIResults_testresult"."status_id", "CIResults_testresult"."url", '
354 '"CIResults_testresult"."start", "CIResults_testresult"."duration", "CIResults_testresult"."command", '
355 '"CIResults_testresult"."stdout", "CIResults_testresult"."stderr", "CIResults_testresult"."dmesg" FROM '
356 '"CIResults_testresult" WHERE "CIResults_testresult"."stdout" = out'),
357 str(parser.objects.query)
358 )
360 def test_equal_m2m_multiple(self):
361 machine = baker.make(Machine, tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")])
362 test_result = baker.make(TestResult, ts_run__machine=machine)
363 parser = QueryParser(TestResult, "machine_tag = 'tag1' AND machine_tag = 'tag2'")
364 self.assertTrue(parser.is_valid)
365 self.assertIn(test_result, parser.objects)
366 self.assertNotIn(test_result, QueryParser(TestResult, "machine_tag = 'tag1' AND machine_tag = 'tag3'").objects)
369class PythonQueryParserTests(TestCase):
370 def test_empty_query(self):
371 test_result = baker.make(TestResult)
372 self.assertTrue(QueryParserPython(TestResult, "").matching_fn(test_result))
374 def test_invalid_query(self):
375 self.assertFalse(QueryParserPython(TestResult, "invalid query").is_valid)
376 self.assertFalse(QueryParserPython(TestResult, "status_name").is_valid)
377 self.assertFalse(QueryParserPython(TestResult, "status_name = 'fail' AND").is_valid)
379 def test_equal_query(self):
380 self.assertTrue(
381 QueryParserPython(TestResult, "status_name = 'pass'").matching_fn(
382 baker.make(TestResult, status__name="pass")
383 )
384 )
385 self.assertFalse(
386 QueryParserPython(TestResult, "status_name = 'fail'").matching_fn(
387 baker.make(TestResult, status__name="pass")
388 )
389 )
391 def test_equal_m2m(self):
392 test_result = baker.make(
393 TestResult, ts_run__machine__tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")]
394 )
395 self.assertTrue(QueryParserPython(TestResult, "machine_tag = 'tag1'").matching_fn(test_result))
396 self.assertTrue(QueryParserPython(TestResult, "machine_tag = 'tag2'").matching_fn(test_result))
397 self.assertFalse(QueryParserPython(TestResult, "machine_tag = 'tag3'").matching_fn(test_result))
399 def test_equal_m2m_multiple(self):
400 test_result = baker.make(
401 TestResult, ts_run__machine__tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")]
402 )
403 self.assertTrue(
404 QueryParserPython(TestResult, "machine_tag = 'tag1' AND machine_tag = 'tag2'").matching_fn(test_result)
405 )
407 def test_not_equal_query(self):
408 self.assertTrue(
409 QueryParserPython(TestResult, "status_name != 'fail'").matching_fn(
410 baker.make(TestResult, status__name="pass")
411 )
412 )
413 self.assertFalse(
414 QueryParserPython(TestResult, "status_name != 'pass'").matching_fn(
415 baker.make(TestResult, status__name="pass")
416 )
417 )
419 def test_not_prefix(self):
420 self.assertTrue(
421 QueryParserPython(TestResult, "NOT status_name = 'fail'").matching_fn(
422 baker.make(TestResult, status__name="pass")
423 )
424 )
425 self.assertFalse(
426 QueryParserPython(TestResult, "NOT status_name = 'pass'").matching_fn(
427 baker.make(TestResult, status__name="pass")
428 )
429 )
431 def test_contains(self):
432 self.assertTrue(
433 QueryParserPython(TestResult, "status_name CONTAINS 'ss'").matching_fn(
434 baker.make(TestResult, status__name="pass")
435 )
436 )
437 self.assertFalse(
438 QueryParserPython(TestResult, "status_name CONTAINS 'xx'").matching_fn(
439 baker.make(TestResult, status__name="pass")
440 )
441 )
443 def test_icontains(self):
444 self.assertTrue(
445 QueryParserPython(TestResult, "status_name ICONTAINS 'SS'").matching_fn(
446 baker.make(TestResult, status__name="pass")
447 )
448 )
450 def test_is_in(self):
451 self.assertTrue(
452 QueryParserPython(TestResult, "status_name IS IN ['pass', 'fail']").matching_fn(
453 baker.make(TestResult, status__name="pass")
454 )
455 )
456 self.assertFalse(
457 QueryParserPython(TestResult, "status_name IS IN ['fail']").matching_fn(
458 baker.make(TestResult, status__name="pass")
459 )
460 )
462 def test_not_in(self):
463 self.assertTrue(
464 QueryParserPython(TestResult, "status_name NOT IN ['fail', 'abort']").matching_fn(
465 baker.make(TestResult, status__name="pass")
466 )
467 )
468 self.assertFalse(
469 QueryParserPython(TestResult, "status_name NOT IN ['fail', 'pass']").matching_fn(
470 baker.make(TestResult, status__name="pass")
471 )
472 )
474 def test_is_in_m2m(self):
475 test_result = baker.make(
476 TestResult, ts_run__machine__tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")]
477 )
478 self.assertTrue(QueryParserPython(TestResult, "machine_tag IS IN ['tag1']").matching_fn(test_result))
479 self.assertTrue(
480 QueryParserPython(TestResult, "machine_tag IS IN ['tag1', 'tag2', 'tag3']").matching_fn(test_result)
481 )
482 self.assertFalse(QueryParserPython(TestResult, "machine_tag IS IN ['tag3']").matching_fn(test_result))
484 def test_contains_m2m(self):
485 test_result = baker.make(
486 TestResult, ts_run__machine__tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")]
487 )
488 self.assertTrue(QueryParserPython(TestResult, "machine_tag CONTAINS ['tag1']").matching_fn(test_result))
489 self.assertTrue(QueryParserPython(TestResult, "machine_tag CONTAINS ['tag1', 'tag2']").matching_fn(test_result))
490 self.assertFalse(
491 QueryParserPython(TestResult, "machine_tag CONTAINS ['tag1', 'tag2', 'tag3']").matching_fn(test_result)
492 )
494 def test_matches(self):
495 test_result = baker.make(TestResult, ts_run__machine__name="gpu_123")
496 self.assertTrue(QueryParserPython(TestResult, "machine_name MATCHES 'gpu'").matching_fn(test_result))
497 self.assertTrue(QueryParserPython(TestResult, "machine_name ~= 'gpu'").matching_fn(test_result))
498 self.assertTrue(QueryParserPython(TestResult, "machine_name MATCHES 'gpu_\\d+'").matching_fn(test_result))
499 self.assertFalse(QueryParserPython(TestResult, "machine_name MATCHES 'gpu$'").matching_fn(test_result))
501 def test_or_operator(self):
502 test_result = baker.make(TestResult, status__name="fail")
503 self.assertTrue(
504 QueryParserPython(TestResult, "status_name = 'fail' OR status_name = 'pass'").matching_fn(test_result)
505 )
506 self.assertTrue(
507 QueryParserPython(TestResult, "status_name = 'pass' OR status_name = 'fail'").matching_fn(test_result)
508 )
509 self.assertFalse(
510 QueryParserPython(TestResult, "status_name = 'pass' OR status_name = 'abort'").matching_fn(test_result)
511 )
513 def test_or_operator__triple(self):
514 test_result = baker.make(TestResult, status__name="fail")
515 self.assertTrue(
516 QueryParserPython(
517 TestResult, "status_name = 'fail' OR status_name = 'pass' OR status_name = 'abort'"
518 ).matching_fn(test_result)
519 )
520 self.assertTrue(
521 QueryParserPython(
522 TestResult, "status_name = 'pass' OR status_name = 'fail' OR status_name = 'abort'"
523 ).matching_fn(test_result)
524 )
525 self.assertTrue(
526 QueryParserPython(
527 TestResult, "status_name = 'pass' OR status_name = 'abort' OR status_name = 'fail'"
528 ).matching_fn(test_result)
529 )
531 def test_or_operator__multiple_fields(self):
532 test_result = baker.make(TestResult, status__name="fail", test__name="test_1")
533 self.assertTrue(
534 QueryParserPython(TestResult, "status_name = 'fail' OR test_name = 'test_1'").matching_fn(test_result)
535 )
536 self.assertTrue(
537 QueryParserPython(TestResult, "status_name = 'pass' OR test_name = 'test_1'").matching_fn(test_result)
538 )
539 self.assertTrue(
540 QueryParserPython(TestResult, "status_name = 'fail' OR test_name = 'test_2'").matching_fn(test_result)
541 )
542 self.assertFalse(
543 QueryParserPython(TestResult, "status_name = 'pass' OR test_name = 'test_2'").matching_fn(test_result)
544 )
546 def test_and_operator(self):
547 test_result = baker.make(TestResult, status__name="fail")
548 self.assertTrue(
549 QueryParserPython(TestResult, "status_name = 'fail' AND status_name = 'fail'").matching_fn(test_result)
550 )
551 self.assertFalse(
552 QueryParserPython(TestResult, "status_name = 'pass' AND status_name = 'fail'").matching_fn(test_result)
553 )
554 self.assertFalse(
555 QueryParserPython(TestResult, "status_name = 'fail' AND status_name = 'pass'").matching_fn(test_result)
556 )
558 def test_and_operator__triple(self):
559 test_result = baker.make(TestResult, status__name="fail")
560 self.assertTrue(
561 QueryParserPython(
562 TestResult, "status_name = 'fail' AND status_name = 'fail' AND status_name = 'fail'"
563 ).matching_fn(test_result)
564 )
565 self.assertFalse(
566 QueryParserPython(
567 TestResult, "status_name = 'pass' AND status_name = 'fail' AND status_name = 'fail'"
568 ).matching_fn(test_result)
569 )
571 def test_and_operator__multiple_fields(self):
572 test_result = baker.make(TestResult, status__name="fail", test__name="test_1")
573 self.assertTrue(
574 QueryParserPython(TestResult, "status_name = 'fail' AND test_name = 'test_1'").matching_fn(test_result)
575 )
576 self.assertFalse(
577 QueryParserPython(TestResult, "status_name = 'pass' AND test_name = 'test_1'").matching_fn(test_result)
578 )
579 self.assertFalse(
580 QueryParserPython(TestResult, "status_name = 'fail' AND test_name = 'test_2'").matching_fn(test_result)
581 )
583 def test_nested(self):
584 test_result = baker.make(
585 TestResult,
586 ts_run__runconfig__name="run_1",
587 ts_run__runconfig__tags=[baker.make(RunConfigTag, name="tag_1"), baker.make(RunConfigTag, name="tag_2")],
588 )
589 self.assertTrue(
590 QueryParserPython(
591 TestResult, "runconfig MATCHES (name='run_1' AND tag CONTAINS ['tag_1', 'tag_2'])"
592 ).matching_fn(test_result)
593 )
594 self.assertFalse(
595 QueryParserPython(
596 TestResult, "runconfig MATCHES (name='run_1' AND tag CONTAINS ['tag_1', 'tag_none'])"
597 ).matching_fn(test_result)
598 )
600 def test_less_than(self):
601 issue = baker.make(Issue, id=1)
602 self.assertTrue(QueryParserPython(Issue, "id < 2").matching_fn(issue))
603 self.assertFalse(QueryParserPython(Issue, "id < 1").matching_fn(issue))
605 def test_less_than_equal(self):
606 issue = baker.make(Issue, id=1)
607 self.assertTrue(QueryParserPython(Issue, "id <= 2").matching_fn(issue))
608 self.assertTrue(QueryParserPython(Issue, "id <= 1").matching_fn(issue))
609 self.assertFalse(QueryParserPython(Issue, "id <= 0").matching_fn(issue))
611 def test_greater_than(self):
612 issue = baker.make(Issue, id=1)
613 self.assertTrue(QueryParserPython(Issue, "id > 0").matching_fn(issue))
614 self.assertFalse(QueryParserPython(Issue, "id > 1").matching_fn(issue))
616 def test_greater_than_equal(self):
617 issue = baker.make(Issue, id=1)
618 self.assertTrue(QueryParserPython(Issue, "id >= 0").matching_fn(issue))
619 self.assertTrue(QueryParserPython(Issue, "id >= 1").matching_fn(issue))
620 self.assertFalse(QueryParserPython(Issue, "id >= 2").matching_fn(issue))
622 def test_reusability(self):
623 test_fail = baker.make(TestResult, status__name="fail")
624 test_abort = baker.make(TestResult, status__name="abort")
625 test_pass = baker.make(TestResult, status__name="pass")
626 test_results = [test_fail, test_abort, test_pass]
627 matches = QueryParserPython(TestResult, "status_name IS IN ['fail', 'abort']").matching_fn
628 filtered_list = list(filter(matches, test_results))
629 self.assertEqual(filtered_list, [test_fail, test_abort])
631 def test_ignore_fields(self):
632 test_result = baker.make(TestResult, status__name="fail", stdout="")
633 self.assertFalse(
634 QueryParserPython(TestResult, "status_name = 'fail' AND stdout ~= 'err'").matching_fn(test_result)
635 )
636 self.assertTrue(
637 QueryParserPython(
638 TestResult, "status_name = 'fail' AND stdout ~= 'err'", ignore_fields=["stdout"]
639 ).matching_fn(test_result)
640 )
642 def test_brackets(self):
643 query = """((testsuite_name = "testsuite1" AND status_name IS IN ["fail", "abort"])
644 OR (testsuite_name = "testsuite2" AND status_name ="skip"))"""
645 testsuite1 = baker.make(TestSuite, name="testsuite1")
646 testsuite2 = baker.make(TestSuite, name="testsuite2")
647 test_result1 = baker.make(TestResult, status__name="fail", status__testsuite=testsuite1)
648 test_result2 = baker.make(TestResult, status__name="fail", status__testsuite=testsuite2)
649 test_result3 = baker.make(TestResult, status__name="skip", status__testsuite=testsuite2)
650 self.assertTrue(QueryParserPython(TestResult, query).matching_fn(test_result1))
651 self.assertFalse(QueryParserPython(TestResult, query).matching_fn(test_result2))
652 self.assertTrue(QueryParserPython(TestResult, query).matching_fn(test_result3))
655class QueryParsersCompilanceTests(TestCase):
656 def assert_parsers_compilance(self, model, user_query, test_results: list, noise_results: list = []):
657 for test_result in test_results:
658 self.assertTrue(QueryParserPython(model, user_query).matching_fn(test_result))
659 self.assertIn(test_result, QueryParser(model, user_query).objects)
660 self.assertEqual(QueryParser(model, user_query).objects.count(), len(test_results))
662 for noise_result in noise_results:
663 self.assertFalse(QueryParserPython(model, user_query).matching_fn(noise_result))
665 def test_empty(self):
666 test_result = baker.make(TestResult)
667 user_query = ""
668 self.assert_parsers_compilance(TestResult, user_query, [test_result])
670 def test_equal_string(self):
671 test_result = baker.make(TestResult, status__name="pass")
672 noise_result = baker.make(TestResult, status__name="fail")
673 self.assert_parsers_compilance(TestResult, "status_name = 'pass'", [test_result], [noise_result])
674 self.assert_parsers_compilance(TestResult, "status_name = 'abort'", [], [noise_result])
676 def test_equal_empty_string(self):
677 noise_results = [baker.make(TestResult, status__name="pass")]
678 self.assert_parsers_compilance(TestResult, "status_name = ''", [], noise_results)
680 def test_equal_boolen(self):
681 test_model = baker.make(TestModel, boolean=True)
682 noise_model = baker.make(TestModel, boolean=False)
683 self.assert_parsers_compilance(TestModel, "boolean = TRUE", [test_model], [noise_model])
685 def test_equal_integer(self):
686 test_model = baker.make(TestModel, number=123)
687 noise_model = baker.make(TestModel, number=456)
688 self.assert_parsers_compilance(TestModel, "number = 123", [test_model], [noise_model])
690 def test_equal_datetime(self):
691 test_model = baker.make(TestModel, date=datetime.datetime(2000, 1, 1, tzinfo=pytz.utc))
692 noise_model = baker.make(TestModel, date=datetime.datetime(2000, 1, 2, tzinfo=pytz.utc))
693 self.assert_parsers_compilance(TestModel, "date = datetime(2000-01-01)", [test_model], [noise_model])
695 def test_equal_duration(self):
696 test_model = baker.make(TestModel, duration=datetime.timedelta(seconds=60))
697 noise_model = baker.make(TestModel, duration=datetime.timedelta(seconds=120))
698 self.assert_parsers_compilance(TestModel, "duration = duration(00:01:00)", [test_model], [noise_model])
700 def test_equal_json(self):
701 test_model = baker.make(TestModel, json={"key": "value"})
702 noise_models = [baker.make(TestModel, json={"key": "noise"}), baker.make(TestModel, json={"foo": "bar"})]
703 self.assert_parsers_compilance(TestModel, "json.key = 'value'", [test_model], noise_models)
705 def test_equal_m2m(self):
706 test_results = [
707 baker.make(
708 TestResult,
709 ts_run__machine__tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")],
710 )
711 ]
712 noise_results = [baker.make(TestResult, ts_run__machine__tags=[baker.make(MachineTag, name="tag_noise")])]
713 self.assert_parsers_compilance(TestResult, "machine_tag = 'tag1'", test_results, noise_results)
714 self.assert_parsers_compilance(TestResult, "machine_tag = 'tag2'", test_results, noise_results)
715 self.assert_parsers_compilance(TestResult, "machine_tag = 'tag3'", [], noise_results)
717 def test_equal_m2m_multiple(self):
718 machine_tag_1 = baker.make(MachineTag, name="tag1")
719 test_results = [
720 baker.make(
721 TestResult,
722 ts_run__machine__tags=[machine_tag_1, baker.make(MachineTag, name="tag2")],
723 )
724 ]
725 noise_results = [baker.make(TestResult, ts_run__machine__tags=[machine_tag_1])]
726 self.assert_parsers_compilance(
727 TestResult, "machine_tag = 'tag1' AND machine_tag = 'tag2'", test_results, noise_results
728 )
729 self.assert_parsers_compilance(TestResult, "machine_tag = 'tag1' AND machine_tag = 'tag3'", [], noise_results)
731 def test_not_equal_query(self):
732 test_results = [baker.make(TestResult, status__name="pass")]
733 noise_results = [baker.make(TestResult, status__name="fail")]
734 self.assert_parsers_compilance(TestResult, "status_name != 'fail'", test_results, noise_results)
736 def test_not_prefix(self):
737 test_results = [baker.make(TestResult, status__name="pass")]
738 noise_results = [baker.make(TestResult, status__name="fail")]
739 self.assert_parsers_compilance(TestResult, "NOT status_name = 'fail'", test_results, noise_results)
741 def test_contains(self):
742 test_results = [baker.make(TestResult, status__name="pass")]
743 noise_results = [baker.make(TestResult, status__name="fail")]
744 self.assert_parsers_compilance(TestResult, "status_name CONTAINS 'ss'", test_results, noise_results)
745 self.assert_parsers_compilance(TestResult, "status_name CONTAINS 'xx'", [], noise_results)
747 def test_icontains(self):
748 test_results = [baker.make(TestResult, status__name="pass")]
749 noise_results = [baker.make(TestResult, status__name="fail")]
750 self.assert_parsers_compilance(TestResult, "status_name ICONTAINS 'SS'", test_results, noise_results)
752 def test_is_in(self):
753 test_results = [baker.make(TestResult, status__name="pass")]
754 noise_results = [baker.make(TestResult, status__name="abort")]
755 self.assert_parsers_compilance(TestResult, "status_name IS IN ['pass', 'fail']", test_results, noise_results)
756 self.assert_parsers_compilance(TestResult, "status_name IS IN ['fail']", [], noise_results)
758 def test_not_in(self):
759 test_results = [baker.make(TestResult, status__name="pass")]
760 noise_results = [baker.make(TestResult, status__name="fail")]
761 self.assert_parsers_compilance(TestResult, "status_name NOT IN ['fail', 'abort']", test_results, noise_results)
762 self.assert_parsers_compilance(TestResult, "status_name NOT IN ['fail', 'pass']", [], noise_results)
764 def test_is_in_m2m(self):
765 test_results = [
766 baker.make(
767 TestResult,
768 ts_run__machine__tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")],
769 )
770 ]
771 noise_results = [baker.make(TestResult, ts_run__machine__tags=[baker.make(MachineTag, name="tag4")])]
772 self.assert_parsers_compilance(TestResult, "machine_tag IS IN ['tag1']", test_results, noise_results)
773 self.assert_parsers_compilance(
774 TestResult, "machine_tag IS IN ['tag1', 'tag2', 'tag3']", test_results, noise_results
775 )
776 self.assert_parsers_compilance(TestResult, "machine_tag IS IN ['tag3']", [], noise_results)
778 def test_contains_m2m(self):
779 test_results = [
780 baker.make(
781 TestResult,
782 ts_run__machine__tags=[baker.make(MachineTag, name="tag1"), baker.make(MachineTag, name="tag2")],
783 )
784 ]
785 noise_results = [baker.make(TestResult, ts_run__machine__tags=[baker.make(MachineTag, name="tag3")])]
786 self.assert_parsers_compilance(TestResult, "machine_tag CONTAINS ['tag1']", test_results, noise_results)
787 self.assert_parsers_compilance(TestResult, "machine_tag CONTAINS ['tag1', 'tag2']", test_results, noise_results)
788 self.assert_parsers_compilance(TestResult, "machine_tag CONTAINS ['tag1', 'tag2', 'tag3']", [])
790 def test_matches(self):
791 test_result_1 = baker.make(TestResult, ts_run__machine__name="gpu")
792 test_result_2 = baker.make(TestResult, ts_run__machine__name="gpu_123")
793 noise_results = [baker.make(TestResult, ts_run__machine__name="cpu")]
795 self.assert_parsers_compilance(
796 TestResult, "machine_name MATCHES 'gpu'", [test_result_1, test_result_2], noise_results
797 )
798 self.assert_parsers_compilance(
799 TestResult, "machine_name ~= 'gpu'", [test_result_1, test_result_2], noise_results
800 )
801 self.assert_parsers_compilance(TestResult, "machine_name MATCHES 'gpu$'", [test_result_1], noise_results)
802 self.assert_parsers_compilance(TestResult, "machine_name MATCHES 'gpu_\\d+'", [test_result_2], noise_results)
803 self.assert_parsers_compilance(TestResult, "machine_name MATCHES 'foo'", [], noise_results)
805 def test_or_operator(self):
806 test_result_1 = baker.make(TestResult, status__name="pass")
807 test_result_2 = baker.make(TestResult, status__name="fail")
808 noise_results = [baker.make(TestResult, status__name="foo")]
809 self.assert_parsers_compilance(
810 TestResult, "status_name = 'fail' OR status_name = 'pass'", [test_result_1, test_result_2], noise_results
811 )
812 self.assert_parsers_compilance(
813 TestResult, "status_name = 'pass' OR status_name = 'fail'", [test_result_1, test_result_2], noise_results
814 )
815 self.assert_parsers_compilance(
816 TestResult, "status_name = 'pass' OR status_name = 'abort'", [test_result_1], noise_results
817 )
818 self.assert_parsers_compilance(
819 TestResult, "status_name = 'fail' OR status_name = 'abort'", [test_result_2], noise_results
820 )
821 self.assert_parsers_compilance(TestResult, "status_name = 'skip' OR status_name = 'abort'", [], noise_results)
823 def test_or_operator__triple(self):
824 test_results = [baker.make(TestResult, status__name="pass")]
825 noise_results = [baker.make(TestResult, status__name="foo")]
826 self.assert_parsers_compilance(
827 TestResult,
828 "status_name = 'fail' OR status_name = 'pass' OR status_name = 'abort'",
829 test_results,
830 noise_results,
831 )
832 self.assert_parsers_compilance(
833 TestResult,
834 "status_name = 'pass' OR status_name = 'fail' OR status_name = 'abort'",
835 test_results,
836 noise_results,
837 )
838 self.assert_parsers_compilance(
839 TestResult,
840 "status_name = 'pass' OR status_name = 'abort' OR status_name = 'fail'",
841 test_results,
842 noise_results,
843 )
845 def test_or_operator__multiple_fields(self):
846 test_results = [baker.make(TestResult, status__name="fail", test__name="test_1")]
847 noise_results = [baker.make(TestResult, status__name="foo")]
848 self.assert_parsers_compilance(
849 TestResult, "status_name = 'fail' OR test_name = 'test_1'", test_results, noise_results
850 )
851 self.assert_parsers_compilance(
852 TestResult, "status_name = 'pass' OR test_name = 'test_1'", test_results, noise_results
853 )
854 self.assert_parsers_compilance(
855 TestResult, "status_name = 'fail' OR test_name = 'test_2'", test_results, noise_results
856 )
857 self.assert_parsers_compilance(TestResult, "status_name = 'pass' OR test_name = 'test_2'", [], noise_results)
859 def test_and_operator(self):
860 test_results = [baker.make(TestResult, status__name="fail")]
861 noise_results = [baker.make(TestResult, status__name="foo")]
862 self.assert_parsers_compilance(
863 TestResult, "status_name = 'fail' AND status_name = 'fail'", test_results, noise_results
864 )
865 self.assert_parsers_compilance(TestResult, "status_name = 'pass' AND status_name = 'fail'", [], noise_results)
866 self.assert_parsers_compilance(TestResult, "status_name = 'fail' AND status_name = 'pass'", [], noise_results)
868 def test_and_operator__triple(self):
869 test_results = [baker.make(TestResult, status__name="fail")]
870 noise_results = [baker.make(TestResult, status__name="foo")]
871 self.assert_parsers_compilance(
872 TestResult,
873 "status_name = 'fail' AND status_name = 'fail' AND status_name = 'fail'",
874 test_results,
875 noise_results,
876 )
877 self.assert_parsers_compilance(
878 TestResult, "status_name = 'pass' AND status_name = 'fail' AND status_name = 'fail'", [], noise_results
879 )
881 def test_and_operator__multiple_fields(self):
882 test_results = [baker.make(TestResult, status__name="fail", test__name="test_1")]
883 noise_results = [baker.make(TestResult, status__name="foo")]
884 self.assert_parsers_compilance(
885 TestResult, "status_name = 'fail' AND test_name = 'test_1'", test_results, noise_results
886 )
887 self.assert_parsers_compilance(TestResult, "status_name = 'pass' AND test_name = 'test_1'", [], noise_results)
888 self.assert_parsers_compilance(TestResult, "status_name = 'fail' AND test_name = 'test_2'", [], noise_results)
890 def test_nested(self):
891 test_results = [
892 baker.make(
893 TestResult,
894 ts_run__runconfig__name="run_1",
895 ts_run__runconfig__tags=[
896 baker.make(RunConfigTag, name="tag_1"),
897 baker.make(RunConfigTag, name="tag_2"),
898 ],
899 )
900 ]
901 noise_results = [baker.make(TestResult, ts_run__runconfig__name="run_foo")]
902 self.assert_parsers_compilance(
903 TestResult,
904 "runconfig MATCHES (name='run_1' AND tag CONTAINS ['tag_1', 'tag_2'])",
905 test_results,
906 noise_results,
907 )
908 self.assert_parsers_compilance(
909 TestResult, "runconfig MATCHES (name='run_1' AND tag CONTAINS ['tag_1', 'tag_none'])", [], noise_results
910 )
912 def test_less_than_number(self):
913 issue_1 = baker.make(Issue, id=1)
914 issue_2 = baker.make(Issue, id=2)
915 self.assert_parsers_compilance(Issue, "id < 3", [issue_1, issue_2])
916 self.assert_parsers_compilance(Issue, "id < 2", [issue_1])
917 self.assert_parsers_compilance(Issue, "id < 1", [])
919 def test_less_than_duration(self):
920 test_model_1 = baker.make(TestModel, duration=datetime.timedelta(seconds=30))
921 test_model_2 = baker.make(TestModel, duration=datetime.timedelta(seconds=60))
922 self.assert_parsers_compilance(TestModel, "duration < duration(00:02:00)", [test_model_1, test_model_2])
923 self.assert_parsers_compilance(TestModel, "duration < duration(00:01:00)", [test_model_1])
924 self.assert_parsers_compilance(TestModel, "duration < duration(00:00:30)", [])
926 def test_less_than_datetime(self):
927 test_model_1 = baker.make(TestModel, date=datetime.datetime(2000, 1, 1, tzinfo=pytz.utc))
928 test_model_2 = baker.make(TestModel, date=datetime.datetime(2000, 1, 2, tzinfo=pytz.utc))
929 self.assert_parsers_compilance(TestModel, "date < datetime(2000-01-03)", [test_model_1, test_model_2])
930 self.assert_parsers_compilance(TestModel, "date < datetime(2000-01-02)", [test_model_1])
931 self.assert_parsers_compilance(TestModel, "date < datetime(2000-01-01)", [])
933 def test_less_than_equal_number(self):
934 issue_1 = baker.make(Issue, id=1)
935 issue_2 = baker.make(Issue, id=2)
936 self.assert_parsers_compilance(Issue, "id <= 3", [issue_1, issue_2])
937 self.assert_parsers_compilance(Issue, "id <= 2", [issue_1, issue_2])
938 self.assert_parsers_compilance(Issue, "id <= 1", [issue_1])
939 self.assert_parsers_compilance(Issue, "id <= 0", [])
941 def test_greater_than(self):
942 issue_1 = baker.make(Issue, id=1)
943 issue_2 = baker.make(Issue, id=2)
944 self.assert_parsers_compilance(Issue, "id > 0", [issue_1, issue_2])
945 self.assert_parsers_compilance(Issue, "id > 1", [issue_2])
946 self.assert_parsers_compilance(Issue, "id > 2", [])
948 def test_greater_than_equal(self):
949 issue_1 = baker.make(Issue, id=1)
950 issue_2 = baker.make(Issue, id=2)
951 self.assert_parsers_compilance(Issue, "id >= 0", [issue_1, issue_2])
952 self.assert_parsers_compilance(Issue, "id >= 1", [issue_1, issue_2])
953 self.assert_parsers_compilance(Issue, "id >= 2", [issue_2])
954 self.assert_parsers_compilance(Issue, "id >= 3", [])
956 def test_brackets(self):
957 query = """((testsuite_name = "testsuite1" AND status_name IS IN ["fail", "abort"])
958 OR (testsuite_name = "testsuite2" AND status_name ="skip"))"""
959 testsuite1 = baker.make(TestSuite, name="testsuite1")
960 testsuite2 = baker.make(TestSuite, name="testsuite2")
961 test_result1 = baker.make(TestResult, status__name="fail", status__testsuite=testsuite1)
962 test_result2 = baker.make(TestResult, status__name="skip", status__testsuite=testsuite2)
963 noise_results = [baker.make(TestResult, status__name="fail", status__testsuite=testsuite2)]
965 self.assert_parsers_compilance(TestResult, query, [test_result1, test_result2], noise_results)
968class LegacyParserTests(TestCase):
969 def setUp(self):
970 UserFiltrableMixin.filter_objects_to_db = {
971 "user_abc": FilterObjectStr('db__abc'),
972 "user_def": FilterObjectStr('db__def'),
973 "user_ghi": FilterObjectStr('db__ghi'),
974 "user_jkl": FilterObjectBool('db__jkl'),
975 "user_mno": FilterObjectDuration('db__mno'),
976 }
978 def test_no_filters(self):
979 parser = LegacyParser(UserFiltrableMixin)
980 self.assertEqual(parser.query, "")
982 def test_valid_filters(self):
983 parser = LegacyParser(UserFiltrableMixin,
984 only__user_abc__in=['toto', 'int(1234.3)'],
985 only__user_def__exact='datetime(2018-06-23)',
986 only__user_ghi__gt=['int(456)'],
987 only__user_jkl__exact='bool(1)',
988 only__user_mno__exact='duration(00:00:03)',
989 exclude__user_def__regex='str(hello)')
990 self.assertEqual(parser.query,
991 "user_abc IS IN ['toto', 1234.3] AND user_def = datetime(2018-06-23) AND user_ghi > 456 "
992 "AND user_jkl = TRUE AND user_mno = duration(00:00:03) AND NOT (user_def ~= 'hello')")
994 def test_regex_aggregation(self):
995 parser = LegacyParser(UserFiltrableMixin, only__user_abc__regex=['toto', 'tata', 'titi'])
996 self.assertEqual(parser.query, "user_abc ~= '(toto|tata|titi)'")
998 def test_invalid_formats(self):
999 parser = LegacyParser(UserFiltrableMixin, balbla='ghujfdk', oops__user_abc__in=12,
1000 only__invalid__in=13, only__user_abc__toto=14)
1001 self.assertEqual(parser.query, "")
1004class UserFiltrableMixinTests(TestCase):
1005 def test_old_style(self):
1006 queryset = TestResult.from_user_filters(only__status_name__exact='pass').objects
1007 self.assertIn('WHERE "CIResults_textstatus"."name" = pass', str(queryset.query))
1009 def test_new_style(self):
1010 queryset = TestResult.from_user_filters(query=['status_name = "pass"']).objects
1011 self.assertIn('WHERE "CIResults_textstatus"."name" = pass', str(queryset.query))
1013 def test_new_style_with_short_queries(self):
1014 q = 'status_name = "toto"'
1015 q2 = 'status_name = "tata"'
1017 # Check that the shorthand versions resolve to the right query
1018 short_query = Shortener.get_or_create(q)
1019 query = TestResult.from_user_filters(query_key=short_query.shorthand)
1020 self.assertEqual(query.user_query, q)
1022 # Check that we prioritize full queries to shorthands
1023 query = TestResult.from_user_filters(query=q2, query_key=short_query.shorthand)
1024 self.assertEqual(query.user_query, q2)
1026 def test_sub_queries(self):
1027 parser = TestResult.from_user_filters(query=['machine_tag CONTAINS ["tag1", "tag2"]'])
1028 sub_query = str(parser.q_objects.children[0][1].query)
1029 self.assertEqual(parser.q_objects.children[0][0], 'ts_run__machine__in')
1030 self.assertIn('"name" = tag1', sub_query)
1031 self.assertIn('"name" = tag2', sub_query)
1034class FilterObjectTests(TestCase):
1035 def test_empty_description(self):
1036 self.assertEqual(FilterObject("").description, "<no description yet>")
1038 def test_with_description(self):
1039 self.assertEqual(FilterObject("", "My description").description, "My description")
1042class FilterObjectDurationTests(TestCase):
1043 def test_invalid_value(self):
1044 with self.assertRaisesRegex(ValueError, "The value '1 month' does not represent a duration"):
1045 FilterObjectDuration.parse_value('1 month')
1048class BuildQueryFromRequestTests(TestCase):
1049 def setUp(self):
1050 self.factory = RequestFactory()
1052 def test_build_machine_query_from_request(self):
1053 request = self.factory.get('/machine?name=name&description=description')
1054 self.assertEqual(QueryCreator(request, Machine).multiple_request_params_to_query().user_query,
1055 "name MATCHES 'name' AND description MATCHES 'description'")