| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import chai from 'chai'; // not esm |
| import { AndExpression, Category, Filter, NotExpression, Operator, OrExpression, parseFilter } from '../src/filter'; |
| |
| before(() => { |
| chai.should(); |
| }); |
| |
| describe('Parser', function () { |
| describe('single item', () => { |
| it('supports common metadata fields', () => { |
| const toTest = { |
| 'any': 'foo', |
| 'manifest': 'foo.cm', |
| 'moniker': 'core/foo', |
| 'message': 'hello', |
| 'package-name': 'my-package', |
| 'tag': 'bar', |
| }; |
| const ops = { |
| 'equal': '=', |
| 'contains': ':' |
| }; |
| for (const [category, value] of Object.entries(toTest)) { |
| for (const [op, opSym] of Object.entries(ops)) { |
| let filters = parseFilter(`${category}${opSym}${value}`); |
| filters.should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: category as Category, |
| subCategory: undefined, |
| operator: op as Operator, |
| value, |
| }), |
| }); |
| |
| |
| filters = parseFilter(`${category}${opSym}/${value.replace('/', '\\/')}/`); |
| filters.should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: category as Category, |
| subCategory: undefined, |
| operator: op as Operator, |
| value: new RegExp(value), |
| }), |
| }); |
| } |
| } |
| }); |
| |
| it('support pid and tid', () => { |
| const ops = { |
| 'equal': '=', |
| 'contains': ':' |
| }; |
| |
| const toTest = { |
| 'pid': 1234, |
| 'tid': 5678, |
| }; |
| for (const [category, value] of Object.entries(toTest)) { |
| for (const [op, opSym] of Object.entries(ops)) { |
| const filters = parseFilter(`${category}${opSym}${value}`); |
| filters.should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: category as Category, |
| subCategory: undefined, |
| operator: op as Operator, |
| value: value, |
| }) |
| }); |
| } |
| } |
| }); |
| |
| it('supports custom keys', () => { |
| const ops = { |
| 'equal': ['=', true], |
| 'contains': [':', 'foo'], |
| 'greaterEq': ['>=', 3], |
| 'lessEq': ['<=', 5], |
| 'greater': ['>', 3], |
| 'less': ['<', 5], |
| }; |
| for (const [op, [opSym, value]] of Object.entries(ops)) { |
| const filters = parseFilter(`some_key${opSym}${value}`); |
| filters.should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'custom', |
| subCategory: 'some_key', |
| operator: op as Operator, |
| value, |
| }) |
| }); |
| } |
| }); |
| |
| it('supports severity levels and only valid severities', () => { |
| const severities = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; |
| const ops = { |
| 'equal': '=', |
| 'contains': ':', |
| 'greaterEq': '>=', |
| 'lessEq': '<=', |
| 'greater': '>', |
| 'less': '<', |
| }; |
| for (const severity of severities) { |
| for (const [op, opSym] of Object.entries(ops)) { |
| const filters = parseFilter(`severity${opSym}${severity}`); |
| filters.should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: op as Operator, |
| value: severity, |
| }) |
| }); |
| } |
| } |
| // An invalid severity is treated as a custom key. |
| parseFilter('severity:foo').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'custom', |
| subCategory: 'severity', |
| operator: 'contains', |
| value: 'foo', |
| }) |
| }); |
| }); |
| |
| it('supports warning as an alias of warn', () => { |
| parseFilter('severity:warning').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'warn', |
| }) |
| }); |
| }); |
| |
| it('supports negations', () => { |
| const ops = ['!', 'not ']; |
| for (const op of ops) { |
| const filters = parseFilter(`${op}severity:info`); |
| filters.should.deep.equal({ |
| ok: true, |
| value: new NotExpression(new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'info', |
| })) |
| }); |
| } |
| }); |
| |
| it('supports spaces and reserved chars inside quotes', () => { |
| const filters = parseFilter('message:"this is:some message"'); |
| filters.should.deep.equal({ |
| ok: true, |
| value: |
| new Filter({ |
| category: 'message', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'this is:some message', |
| }) |
| }); |
| }); |
| |
| it('supports multiple ORed values', () => { |
| parseFilter('tag=foo|"bar baz"|quux').should.deep.equal({ |
| ok: true, |
| value: new OrExpression([ |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'foo', |
| }), |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'bar baz', |
| }), |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'quux', |
| }), |
| ]), |
| }); |
| |
| parseFilter('foo=3|4').should.deep.equal({ |
| ok: true, |
| value: new OrExpression([ |
| new Filter({ |
| category: 'custom', |
| subCategory: 'foo', |
| operator: 'equal', |
| value: 3, |
| }), |
| new Filter({ |
| category: 'custom', |
| subCategory: 'foo', |
| operator: 'equal', |
| value: 4, |
| }), |
| ]) |
| }); |
| |
| parseFilter('severity=info|debug').should.deep.equal({ |
| ok: true, |
| value: new OrExpression([ |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'info', |
| }), |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'debug', |
| }), |
| ]), |
| }); |
| }); |
| |
| it('supports multiple ANDed values', () => { |
| parseFilter('tag=foo&"bar baz"&quux').should.deep.equal({ |
| ok: true, |
| value: new AndExpression([ |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'foo', |
| }), |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'bar baz', |
| }), |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'quux', |
| }), |
| ]), |
| }); |
| |
| parseFilter('foo=3&4').should.deep.equal({ |
| ok: true, |
| value: new AndExpression([ |
| new Filter({ |
| category: 'custom', |
| subCategory: 'foo', |
| operator: 'equal', |
| value: 3, |
| }), |
| new Filter({ |
| category: 'custom', |
| subCategory: 'foo', |
| operator: 'equal', |
| value: 4, |
| }), |
| ]) |
| }); |
| |
| parseFilter('severity=info&debug').should.deep.equal({ |
| ok: true, |
| value: new AndExpression([ |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'info', |
| }), |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'debug', |
| }), |
| ]), |
| }); |
| }); |
| |
| it('is case insensitive for reserved keywords', () => { |
| parseFilter('SeveriTy:info').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'info', |
| }) |
| }); |
| }); |
| |
| it('is case senstive for custom keys', () => { |
| parseFilter('SOME_key=true').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'custom', |
| subCategory: 'SOME_key', |
| operator: 'equal', |
| value: true, |
| }) |
| }); |
| }); |
| }); |
| |
| it('accepts ORed items', () => { |
| parseFilter('moniker:foo OR !severity<info').should.deep.equal( |
| { |
| ok: true, |
| value: |
| new OrExpression([ |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo', |
| }), |
| new NotExpression(new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'less', |
| value: 'info', |
| })), |
| ]) |
| }); |
| |
| parseFilter('(moniker:foo | !tag:bar Or severity=info)').should.deep.equal( |
| { |
| ok: true, |
| value: |
| new OrExpression([ |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo', |
| }), |
| new NotExpression(new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'bar', |
| })), |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'info', |
| }) |
| ]) |
| }); |
| }); |
| |
| it('accepts multiple ANDed expressions', () => { |
| parseFilter('(moniker:foo or !tag:bar|"baz quux") severity:info & tag:foo and message=bar') |
| .should.deep.equal({ |
| ok: true, |
| value: new AndExpression( |
| [ |
| new OrExpression([ |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo', |
| }), |
| new NotExpression(new OrExpression([ |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'bar', |
| }), |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'baz quux', |
| }), |
| ])), |
| ]), |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'info', |
| }), |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo', |
| }), |
| new Filter({ |
| category: 'message', |
| subCategory: undefined, |
| operator: 'equal', |
| value: 'bar', |
| }), |
| ] |
| ) |
| }); |
| }); |
| |
| describe('raw strings parser', () => { |
| it('can be used as "any" filters', () => { |
| parseFilter('foo').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo', |
| }) |
| }); |
| }); |
| |
| it('handles escaped chars', () => { |
| parseFilter('foo"').ok.should.be.false; |
| parseFilter('foo\\"').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo"', |
| }) |
| }); |
| |
| parseFilter('"foo""').ok.should.be.false; |
| parseFilter('"foo\\""').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo"', |
| }) |
| }); |
| }); |
| |
| it('accepts multiple raw strings form a conjunction', () => { |
| parseFilter('foo bar').should.deep.equal({ |
| ok: true, |
| value: new AndExpression([ |
| new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo', |
| }), |
| new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'bar', |
| }) |
| ]) |
| }); |
| }); |
| |
| it('accepts multiple strings inside quotes is a single string', () => { |
| parseFilter('"foo bar"').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo bar', |
| }) |
| }); |
| }); |
| |
| it('parses not before the string', () => { |
| parseFilter('NOT foo').should.deep.equal({ |
| ok: true, |
| value: new NotExpression( |
| new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo', |
| }) |
| ) |
| }); |
| parseFilter('!"foo bar"').should.deep.equal({ |
| ok: true, |
| value: new NotExpression( |
| new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo bar', |
| }) |
| ) |
| }); |
| }); |
| |
| it('can be mixed with other filters', () => { |
| parseFilter('moniker:core/bar cool').should.deep.equal({ |
| ok: true, |
| value: new AndExpression([ |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'core/bar', |
| }), |
| new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'cool', |
| }) |
| ]), |
| }); |
| }); |
| }); |
| |
| describe('raw regexes parser', () => { |
| it('accepts raw regexes as input', () => { |
| parseFilter('/foo\\/bar/').should.deep.equal({ |
| ok: true, |
| value: new Filter({ |
| category: 'any', |
| subCategory: undefined, |
| operator: 'contains', |
| value: new RegExp('foo/bar'), |
| }) |
| }); |
| }); |
| }); |
| |
| describe('parse logical expressions', () => { |
| it('supports nesting arbitrary expressions', () => { |
| parseFilter('(moniker:foo or moniker:bar) severity:info (tag:foo or !(foo=bar and !k<2))') |
| .should.deep.equal({ |
| ok: true, |
| value: new AndExpression([ |
| new OrExpression([ |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo' |
| }), |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'bar' |
| }) |
| ]), |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'info' |
| }), |
| new OrExpression([ |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo' |
| }), |
| new NotExpression(new AndExpression([ |
| new Filter({ |
| category: 'custom', |
| subCategory: 'foo', |
| operator: 'equal', |
| value: 'bar' |
| }), |
| new NotExpression(new Filter({ |
| category: 'custom', |
| subCategory: 'k', |
| operator: 'less', |
| value: 2 |
| })) |
| ])), |
| ]) |
| ]) |
| }); |
| |
| parseFilter('!(((moniker:foo or severity:info) & tag:bar) or package-name:baz) message:crash') |
| .should.deep.equal({ |
| ok: true, |
| value: new AndExpression([ |
| new NotExpression( |
| new OrExpression([ |
| new AndExpression([ |
| new OrExpression([ |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'foo' |
| }), |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'info' |
| }), |
| ]), |
| new Filter({ |
| category: 'tag', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'bar' |
| }) |
| ]), |
| new Filter({ |
| category: 'package-name', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'baz' |
| }), |
| ]), |
| ), |
| new Filter({ |
| category: 'message', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'crash' |
| }) |
| ]), |
| }); |
| }); |
| |
| it('makes conjunction take precedence over disjunction', () => { |
| parseFilter('severity:info and moniker:bar or moniker:baz').should.deep.equal({ |
| ok: true, |
| value: new OrExpression([ |
| new AndExpression([ |
| new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'info' |
| }), |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'bar' |
| }) |
| ]), |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'baz' |
| }) |
| ]), |
| }); |
| }); |
| |
| it('makes not take precedence over and', () => { |
| parseFilter('not severity:info and moniker:bar or not moniker:baz').should.deep.equal({ |
| ok: true, |
| value: new OrExpression([ |
| new AndExpression([ |
| new NotExpression(new Filter({ |
| category: 'severity', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'info' |
| })), |
| new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'bar' |
| }) |
| ]), |
| new NotExpression(new Filter({ |
| category: 'moniker', |
| subCategory: undefined, |
| operator: 'contains', |
| value: 'baz' |
| })) |
| ]), |
| }); |
| }); |
| }); |
| }); |