github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/atoms.go (about)

     1  // Copyright 2018 The WPT Dashboard Project. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package query
     6  
     7  // This file defines search atoms on the backend. All wpt.fyi search queries
     8  // are broken down into a tree of search atoms, which is then traversed by the
     9  // searchcache to find matching tests to include.
    10  //
    11  // All search atoms must define two methods:
    12  //   i. BindToRuns
    13  //   ii. UnmarshalJSON
    14  //
    15  // These are best understood in reverse, as that is also the order they are
    16  // called in. UnmarshalJSON is used to convert from the original JSON search
    17  // query into the tree of abstract search atoms. The atoms are referred to as
    18  // abstract as they do not yet relate to any underlying data (i.e. any test
    19  // runs). Many types of atom (such as AbstractExists) perform this
    20  // unmarshalling recursively, which is how we end up with a tree.
    21  //
    22  // Once we have an abstract search tree, BindToRuns will convert it to a
    23  // concrete search tree (that is, a tree of ConcreteQuery atoms). This gives
    24  // the search atoms access to the specific runs that are being searched over,
    25  // to pull any specific information needed. For example, this allows
    26  // TestStatusEq to only produce results for test runs that match the specified
    27  // product (and short-circuit entirely if no test runs match).
    28  //
    29  // Some abstract search atoms may produce more than one concrete search atom
    30  // (e.g. AbstractExists, which produces a disjunction), whilst others may
    31  // ignore the test runs entirely if they aren't relevant (e.g.
    32  // TestNamePattern, which only cares about the test name and not the results).
    33  //
    34  // Note that this file does not perform the actual filtering of tests from the
    35  // test runs to produce the search response; for that see the `filter` type in
    36  // api/query/cache/index/filter.go
    37  
    38  import (
    39  	"encoding/json"
    40  	"errors"
    41  	"fmt"
    42  	"strings"
    43  
    44  	"github.com/sirupsen/logrus"
    45  	"github.com/web-platform-tests/wpt.fyi/shared"
    46  )
    47  
    48  // AbstractQuery is an intermetidate representation of a test results query that
    49  // has not been bound to specific shared.TestRun specs for processing.
    50  type AbstractQuery interface {
    51  	BindToRuns(runs ...shared.TestRun) ConcreteQuery
    52  }
    53  
    54  // RunQuery is the internal representation of a query received from an HTTP
    55  // client, including the IDs of the test runs to query, and the structured query
    56  // to run.
    57  type RunQuery struct {
    58  	RunIDs []int64
    59  	AbstractQuery
    60  }
    61  
    62  // True is a true-valued ConcreteQuery.
    63  type True struct{}
    64  
    65  // BindToRuns for True is a no-op; it is independent of test runs.
    66  // nolint:ireturn // TODO: Fix ireturn lint error
    67  func (t True) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
    68  	return t
    69  }
    70  
    71  // False is a false-valued ConcreteQuery.
    72  type False struct{}
    73  
    74  // BindToRuns for False is a no-op; it is independent of test runs.
    75  // nolint:ireturn // TODO: Fix ireturn lint error
    76  func (f False) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
    77  	return f
    78  }
    79  
    80  // TestNamePattern is a query atom that matches test names to a pattern string.
    81  type TestNamePattern struct {
    82  	Pattern string
    83  }
    84  
    85  // BindToRuns for TestNamePattern is a no-op; it is independent of test runs.
    86  // nolint:ireturn // TODO: Fix ireturn lint error
    87  func (tnp TestNamePattern) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
    88  	return tnp
    89  }
    90  
    91  // SubtestNamePattern is a query atom that matches subtest names to a pattern string.
    92  type SubtestNamePattern struct {
    93  	Subtest string
    94  }
    95  
    96  // BindToRuns for SubtestNamePattern is a no-op; it is independent of test runs.
    97  // nolint:ireturn // TODO: Fix ireturn lint error
    98  func (tnp SubtestNamePattern) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
    99  	return tnp
   100  }
   101  
   102  // TestPath is a query atom that matches exact test path prefixes.
   103  // It is an inflexible equivalent of TestNamePattern.
   104  type TestPath struct {
   105  	Path string
   106  }
   107  
   108  // BindToRuns for TestNamePattern is a no-op; it is independent of test runs.
   109  // nolint:ireturn // TODO: Fix ireturn lint error
   110  func (tp TestPath) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
   111  	return tp
   112  }
   113  
   114  // AbstractExists represents an array of abstract queries, each of which must be
   115  // satifisfied by some run. It represents the root of a structured query.
   116  type AbstractExists struct {
   117  	Args []AbstractQuery
   118  }
   119  
   120  // BindToRuns binds each abstract query to an or-combo of that query against
   121  // each specific/individual run.
   122  // nolint:ireturn // TODO: Fix ireturn lint error
   123  func (e AbstractExists) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   124  	queries := make([]ConcreteQuery, len(e.Args))
   125  	// When the nested query is a single query, e.g. And/Or, bind that query directly.
   126  	if len(e.Args) == 1 {
   127  		return e.Args[0].BindToRuns(runs...)
   128  	}
   129  
   130  	for i, arg := range e.Args {
   131  		var query ConcreteQuery
   132  		// Exists queries are split; one run must satisfy the whole tree.
   133  		byRun := make([]ConcreteQuery, 0, len(runs))
   134  		for _, run := range runs {
   135  			bound := arg.BindToRuns(run)
   136  			if _, ok := bound.(False); !ok {
   137  				byRun = append(byRun, bound)
   138  			}
   139  		}
   140  		query = Or{Args: byRun}
   141  		queries[i] = query
   142  	}
   143  	// And the overall node is true if all its exists queries are true.
   144  	return And{
   145  		Args: queries,
   146  	}
   147  }
   148  
   149  // AbstractAll represents an array of abstract queries, each of which must be
   150  // satifisfied by all runs. It represents the root of a structured query.
   151  type AbstractAll struct {
   152  	Args []AbstractQuery
   153  }
   154  
   155  // BindToRuns binds each abstract query to an and-combo of that query against
   156  // each specific/individual run.
   157  // nolint:ireturn // TODO: Fix ireturn lint error
   158  func (e AbstractAll) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   159  	queries := make([]ConcreteQuery, len(e.Args))
   160  	for i, arg := range e.Args {
   161  		var query ConcreteQuery
   162  		byRun := make([]ConcreteQuery, 0, len(runs))
   163  		for _, run := range runs {
   164  			bound := arg.BindToRuns(run)
   165  			if _, ok := bound.(True); !ok { // And with True is pointless.
   166  				byRun = append(byRun, bound)
   167  			}
   168  		}
   169  		query = And{Args: byRun}
   170  		queries[i] = query
   171  	}
   172  	// And the overall node is true if all its exists queries are true.
   173  	return And{
   174  		Args: queries,
   175  	}
   176  }
   177  
   178  // AbstractNone represents an array of abstract queries, each of which must not be
   179  // satifisfied by any run. It represents the root of a structured query.
   180  type AbstractNone struct {
   181  	Args []AbstractQuery
   182  }
   183  
   184  // BindToRuns binds to a not-exists for the same query(s).
   185  // nolint:ireturn // TODO: Fix ireturn lint error
   186  func (e AbstractNone) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   187  	return Not{
   188  		AbstractExists(e).BindToRuns(runs...),
   189  	}
   190  }
   191  
   192  // AbstractSequential represents the root of a sequential queries, where the first
   193  // query must be satisfied by some run such that the next run, sequentially, also
   194  // satisfies the next query, and so on.
   195  type AbstractSequential struct {
   196  	Args []AbstractQuery
   197  }
   198  
   199  // BindToRuns binds each sequential query to an and-combo of those queries against
   200  // specific sequential runs, for each combination of sequential runs.
   201  // nolint:ireturn // TODO: Fix ireturn lint error
   202  func (e AbstractSequential) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   203  	numSeqQueries := len(e.Args)
   204  	byRuns := []ConcreteQuery{}
   205  	for i := 0; i+numSeqQueries-1 < len(runs); i++ {
   206  		all := And{} // nolint:exhaustruct // TODO: Fix exhaustruct lint error.
   207  		for j, arg := range e.Args {
   208  			all.Args = append(all.Args, arg.BindToRuns(runs[i+j]))
   209  		}
   210  		byRuns = append(byRuns, all)
   211  	}
   212  
   213  	return Or{
   214  		Args: byRuns,
   215  	}
   216  }
   217  
   218  // AbstractCount represents the root of a count query, where the exact number of
   219  // runs that satisfy the query must match the expected count.
   220  type AbstractCount struct {
   221  	Count int
   222  	Where AbstractQuery
   223  }
   224  
   225  // BindToRuns binds each count query to all of the runs, so that it can count the
   226  // number of runs that match the criteria.
   227  // nolint:ireturn // TODO: Fix ireturn lint error
   228  func (c AbstractCount) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   229  	byRun := []ConcreteQuery{}
   230  	for _, run := range runs {
   231  		byRun = append(byRun, c.Where.BindToRuns(run))
   232  	}
   233  
   234  	return Count{
   235  		Count: c.Count,
   236  		Args:  byRun,
   237  	}
   238  }
   239  
   240  // AbstractMoreThan is the root of a moreThan query, where the number of runs
   241  // that satisfy the query must be more than the given count.
   242  type AbstractMoreThan struct {
   243  	AbstractCount
   244  }
   245  
   246  // BindToRuns binds each count query to all of the runs, so that it can count the
   247  // number of runs that match the criteria.
   248  // nolint:ireturn // TODO: Fix ireturn lint error
   249  func (m AbstractMoreThan) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   250  	c := m.AbstractCount.BindToRuns(runs...).(Count)
   251  
   252  	return MoreThan{c}
   253  }
   254  
   255  // AbstractLessThan is the root of a lessThan query, where the number of runs
   256  // that satisfy the query must be less than the given count.
   257  type AbstractLessThan struct {
   258  	AbstractCount
   259  }
   260  
   261  // BindToRuns binds each count query to all of the runs, so that it can count the
   262  // number of runs that match the criteria.
   263  // nolint:ireturn // TODO: Fix ireturn lint error
   264  func (l AbstractLessThan) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   265  	c := l.AbstractCount.BindToRuns(runs...).(Count)
   266  
   267  	return LessThan{c}
   268  }
   269  
   270  // AbstractLink is represents the root of a link query, which matches Metadata
   271  // URLs to a pattern string.
   272  type AbstractLink struct {
   273  	Pattern         string
   274  	metadataFetcher shared.MetadataFetcher
   275  }
   276  
   277  // BindToRuns for AbstractLink fetches metadata for either test-level issues or
   278  // issues associated with the given runs. It does not filter the metadata by
   279  // the pattern yet.
   280  // nolint:ireturn // TODO: Fix ireturn lint error
   281  func (l AbstractLink) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   282  	if l.metadataFetcher == nil {
   283  		l.metadataFetcher = searchcacheMetadataFetcher{}
   284  	}
   285  	includeTestLevel := true
   286  	metadata, _ := shared.GetMetadataResponse(runs, includeTestLevel, logrus.StandardLogger(), l.metadataFetcher)
   287  	metadataMap := shared.PrepareLinkFilter(metadata)
   288  
   289  	return Link{
   290  		Pattern:  l.Pattern,
   291  		Metadata: metadataMap,
   292  	}
   293  }
   294  
   295  // AbstractTriaged represents the root of a triaged query that matches
   296  // tests where the test of a specific browser has been triaged through Metadata.
   297  type AbstractTriaged struct {
   298  	Product         *shared.ProductSpec
   299  	metadataFetcher shared.MetadataFetcher
   300  }
   301  
   302  // BindToRuns for AbstractTriaged binds each run matching the AbstractTriaged
   303  // ProductSpec to a triaged object.
   304  // nolint:ireturn // TODO: Fix ireturn lint error
   305  func (t AbstractTriaged) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   306  	cq := make([]ConcreteQuery, 0)
   307  
   308  	if t.metadataFetcher == nil {
   309  		t.metadataFetcher = searchcacheMetadataFetcher{}
   310  	}
   311  	for _, run := range runs {
   312  		if t.Product == nil || t.Product.Matches(run) {
   313  			// We only want to fetch metadata for this specific run (or for no runs, if
   314  			// the search is for test-level issues).
   315  			includeTestLevel := false
   316  			metadataRuns := []shared.TestRun{run}
   317  
   318  			// Product being nil means that we want test-level issues.
   319  			if t.Product == nil {
   320  				includeTestLevel = true
   321  				metadataRuns = []shared.TestRun{}
   322  			}
   323  			metadata, _ := shared.GetMetadataResponse(metadataRuns, includeTestLevel, logrus.StandardLogger(), t.metadataFetcher)
   324  			metadataMap := shared.PrepareLinkFilter(metadata)
   325  			if len(metadataMap) > 0 {
   326  				cq = append(cq, Triaged{run.ID, metadataMap})
   327  			}
   328  		}
   329  	}
   330  
   331  	if len(cq) == 0 {
   332  		return False{}
   333  	}
   334  
   335  	return Or{cq}
   336  }
   337  
   338  // AbstractTestLabel represents the root of a testlabel query, which matches test-level metadata
   339  // labels to a searched label.
   340  type AbstractTestLabel struct {
   341  	Label           string
   342  	metadataFetcher shared.MetadataFetcher
   343  }
   344  
   345  // BindToRuns for AbstractTestLabel fetches test-level metadata; it is independent of test runs.
   346  // nolint:ireturn // TODO: Fix ireturn lint error
   347  func (t AbstractTestLabel) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
   348  	if t.metadataFetcher == nil {
   349  		t.metadataFetcher = searchcacheMetadataFetcher{}
   350  	}
   351  
   352  	includeTestLevel := true
   353  	// Passing []shared.TestRun{} means that we want test-level issues.
   354  	metadata, _ := shared.GetMetadataResponse(
   355  		[]shared.TestRun{},
   356  		includeTestLevel,
   357  		logrus.StandardLogger(),
   358  		t.metadataFetcher,
   359  	)
   360  	metadataMap := shared.PrepareTestLabelFilter(metadata)
   361  
   362  	return TestLabel{
   363  		Label:    t.Label,
   364  		Metadata: metadataMap,
   365  	}
   366  }
   367  
   368  // webFeaturesManifestFetcher describes the behavior to fetch Web Features data.
   369  type webFeaturesManifestFetcher interface {
   370  	Fetch() (shared.WebFeaturesData, error)
   371  }
   372  
   373  // AbstractTestWebFeature represents the root of a web_feature query, which matches test-level
   374  // metadata to a searched web feature.
   375  type AbstractTestWebFeature struct {
   376  	TestWebFeatureAtom
   377  	manifestFetcher webFeaturesManifestFetcher
   378  }
   379  
   380  // BindToRuns for AbstractTestWebFeature fetches test-level metadata; it is independent of test runs.
   381  // nolint:ireturn // TODO: Fix ireturn lint error
   382  func (t AbstractTestWebFeature) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
   383  	data, _ := t.manifestFetcher.Fetch()
   384  
   385  	return TestWebFeature{
   386  		WebFeature:      t.WebFeature,
   387  		WebFeaturesData: data,
   388  	}
   389  }
   390  
   391  // MetadataQuality represents the root of an "is" query, which asserts known
   392  // metadata qualities to the results.
   393  type MetadataQuality int
   394  
   395  const (
   396  	// MetadataQualityUnknown is a placeholder for unrecognized values.
   397  	MetadataQualityUnknown MetadataQuality = iota
   398  	// MetadataQualityDifferent represents an is:different atom.
   399  	// "different" ensures that one or more results differs from the other results.
   400  	MetadataQualityDifferent
   401  	// MetadataQualityTentative represents an is:tentative atom.
   402  	// "tentative" ensures that the results are from a tentative test.
   403  	MetadataQualityTentative
   404  	// MetadataQualityOptional represents an is:optional atom.
   405  	// "optional" ensures that the results are from an optional test.
   406  	MetadataQualityOptional
   407  )
   408  
   409  // BindToRuns for MetadataQuality is a no-op; it is independent of test runs.
   410  // nolint:ireturn // TODO: Fix ireturn lint error
   411  func (q MetadataQuality) BindToRuns(_ ...shared.TestRun) ConcreteQuery {
   412  	return q
   413  }
   414  
   415  // TestStatusEq is a query atom that matches tests where the test status/result
   416  // from at least one test run matches the given status value, optionally filtered
   417  // to a specific browser name.
   418  type TestStatusEq struct {
   419  	Product *shared.ProductSpec
   420  	Status  shared.TestStatus
   421  }
   422  
   423  // TestStatusNeq is a query atom that matches tests where the test status/result
   424  // from at least one test run does not match the given status value, optionally
   425  // filtered to a specific browser name.
   426  type TestStatusNeq struct {
   427  	Product *shared.ProductSpec
   428  	Status  shared.TestStatus
   429  }
   430  
   431  // BindToRuns for TestStatusEq expands to a disjunction of RunTestStatusEq
   432  // values.
   433  // nolint:ireturn // TODO: Fix ireturn lint error
   434  func (tse TestStatusEq) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   435  	ids := make([]int64, 0, len(runs))
   436  	for _, run := range runs {
   437  		if tse.Product == nil || tse.Product.Matches(run) {
   438  			ids = append(ids, run.ID)
   439  		}
   440  	}
   441  	if len(ids) == 0 {
   442  		return False{}
   443  	}
   444  	if len(ids) == 1 {
   445  		return RunTestStatusEq{ids[0], tse.Status}
   446  	}
   447  
   448  	q := Or{make([]ConcreteQuery, len(ids))}
   449  	for i := range ids {
   450  		q.Args[i] = RunTestStatusEq{ids[i], tse.Status}
   451  	}
   452  
   453  	return q
   454  }
   455  
   456  // BindToRuns for TestStatusNeq expands to a disjunction of RunTestStatusNeq
   457  // values.
   458  // nolint:ireturn // TODO: Fix ireturn lint error
   459  func (tsn TestStatusNeq) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   460  	ids := make([]int64, 0, len(runs))
   461  	for _, run := range runs {
   462  		if tsn.Product == nil || tsn.Product.Matches(run) {
   463  			ids = append(ids, run.ID)
   464  		}
   465  	}
   466  	if len(ids) == 0 {
   467  		return False{}
   468  	}
   469  	if len(ids) == 1 {
   470  		return RunTestStatusNeq{ids[0], tsn.Status}
   471  	}
   472  
   473  	q := Or{make([]ConcreteQuery, len(ids))}
   474  	for i := range ids {
   475  		q.Args[i] = RunTestStatusNeq{ids[i], tsn.Status}
   476  	}
   477  
   478  	return q
   479  }
   480  
   481  // AbstractNot is the AbstractQuery for negation.
   482  type AbstractNot struct {
   483  	Arg AbstractQuery
   484  }
   485  
   486  // BindToRuns for AbstractNot produces a Not with a bound argument.
   487  // nolint:ireturn // TODO: Fix ireturn lint error
   488  func (n AbstractNot) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   489  	return Not{n.Arg.BindToRuns(runs...)}
   490  }
   491  
   492  // AbstractOr is the AbstractQuery for disjunction.
   493  type AbstractOr struct {
   494  	Args []AbstractQuery
   495  }
   496  
   497  // BindToRuns for AbstractOr produces an Or with bound arguments.
   498  // nolint:ireturn // TODO: Fix ireturn lint error
   499  func (o AbstractOr) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   500  	args := make([]ConcreteQuery, 0, len(o.Args))
   501  	for i := range o.Args {
   502  		sub := o.Args[i].BindToRuns(runs...)
   503  		if t, ok := sub.(True); ok {
   504  			return t
   505  		}
   506  		if _, ok := sub.(False); ok {
   507  			continue
   508  		}
   509  		args = append(args, sub)
   510  	}
   511  	if len(args) == 0 {
   512  		return False{}
   513  	}
   514  	if len(args) == 1 {
   515  		return args[0]
   516  	}
   517  
   518  	return Or{
   519  		Args: args,
   520  	}
   521  }
   522  
   523  // AbstractAnd is the AbstractQuery for conjunction.
   524  type AbstractAnd struct {
   525  	Args []AbstractQuery
   526  }
   527  
   528  // BindToRuns for AbstractAnd produces an And with bound arguments.
   529  // nolint:ireturn // TODO: Fix ireturn lint error
   530  func (a AbstractAnd) BindToRuns(runs ...shared.TestRun) ConcreteQuery {
   531  	args := make([]ConcreteQuery, 0, len(a.Args))
   532  	for i := range a.Args {
   533  		sub := a.Args[i].BindToRuns(runs...)
   534  		if _, ok := sub.(False); ok {
   535  			return False{}
   536  		}
   537  		if _, ok := sub.(True); ok {
   538  			continue
   539  		}
   540  		args = append(args, sub)
   541  	}
   542  	if len(args) == 0 {
   543  		return False{}
   544  	}
   545  	if len(args) == 1 {
   546  		return args[0]
   547  	}
   548  
   549  	return And{
   550  		Args: args,
   551  	}
   552  }
   553  
   554  // UnmarshalJSON interprets the JSON representation of a RunQuery, instantiating
   555  // (an) appropriate Query implementation(s) according to the JSON structure.
   556  func (rq *RunQuery) UnmarshalJSON(b []byte) error {
   557  	var data struct {
   558  		RunIDs []int64         `json:"run_ids"`
   559  		Query  json.RawMessage `json:"query"`
   560  	}
   561  	if err := json.Unmarshal(b, &data); err != nil {
   562  		return err
   563  	}
   564  	if len(data.RunIDs) == 0 {
   565  		return errors.New(`Missing run query property: "run_ids"`)
   566  	}
   567  	rq.RunIDs = data.RunIDs
   568  
   569  	if len(data.Query) > 0 {
   570  		q, err := unmarshalQ(data.Query)
   571  		if err != nil {
   572  			return err
   573  		}
   574  		rq.AbstractQuery = q
   575  	} else {
   576  		rq.AbstractQuery = True{}
   577  	}
   578  
   579  	return nil
   580  }
   581  
   582  // UnmarshalJSON for TestNamePattern attempts to interpret a query atom as
   583  // {"pattern":<test name pattern string>}.
   584  func (tnp *TestNamePattern) UnmarshalJSON(b []byte) error {
   585  	var data map[string]*json.RawMessage
   586  	if err := json.Unmarshal(b, &data); err != nil {
   587  		return err
   588  	}
   589  	patternMsg, ok := data["pattern"]
   590  	if !ok {
   591  		return errors.New(`Missing test name pattern property: "pattern"`)
   592  	}
   593  	var pattern string
   594  	if err := json.Unmarshal(*patternMsg, &pattern); err != nil {
   595  		return errors.New(`test name pattern property "pattern" is not a string`)
   596  	}
   597  
   598  	tnp.Pattern = pattern
   599  
   600  	return nil
   601  }
   602  
   603  // UnmarshalJSON for SubtestNamePattern attempts to interpret a query atom as
   604  // {"subtest":<subtest name pattern string>}.
   605  func (tnp *SubtestNamePattern) UnmarshalJSON(b []byte) error {
   606  	var data map[string]*json.RawMessage
   607  	if err := json.Unmarshal(b, &data); err != nil {
   608  		return err
   609  	}
   610  	subtestMsg, ok := data["subtest"]
   611  	if !ok {
   612  		return errors.New(`Missing subtest name pattern property: "subtest"`)
   613  	}
   614  	var subtest string
   615  	if err := json.Unmarshal(*subtestMsg, &subtest); err != nil {
   616  		return errors.New(`Subtest name property "subtest" is not a string`)
   617  	}
   618  	tnp.Subtest = subtest
   619  
   620  	return nil
   621  }
   622  
   623  // UnmarshalJSON for TestPath attempts to interpret a query atom as
   624  // {"path":<test name pattern string>}.
   625  func (tp *TestPath) UnmarshalJSON(b []byte) error {
   626  	var data map[string]*json.RawMessage
   627  	if err := json.Unmarshal(b, &data); err != nil {
   628  
   629  		return err
   630  	}
   631  	pathMsg, ok := data["path"]
   632  	if !ok {
   633  		return errors.New(`Missing test name path property: "path"`)
   634  	}
   635  	var path string
   636  	if err := json.Unmarshal(*pathMsg, &path); err != nil {
   637  		return errors.New(`Missing test name path property "path" is not a string`)
   638  	}
   639  
   640  	tp.Path = path
   641  
   642  	return nil
   643  }
   644  
   645  // UnmarshalJSON for TestStatusEq attempts to interpret a query atom as
   646  // {"product": <browser name>, "status": <status string>}.
   647  func (tse *TestStatusEq) UnmarshalJSON(b []byte) error {
   648  	var data struct {
   649  		BrowserName string `json:"browser_name"` // Legacy
   650  		Product     string `json:"product"`
   651  		Status      string `json:"status"`
   652  	}
   653  	if err := json.Unmarshal(b, &data); err != nil {
   654  		return err
   655  	}
   656  	if data.Product == "" && data.BrowserName != "" {
   657  		data.Product = data.BrowserName
   658  	}
   659  	if len(data.Status) == 0 {
   660  		return errors.New(`Missing test status constraint property: "status"`)
   661  	}
   662  
   663  	var product *shared.ProductSpec
   664  	if data.Product != "" {
   665  		p, err := shared.ParseProductSpec(data.Product)
   666  		if err != nil {
   667  			return err
   668  		}
   669  		product = &p
   670  	}
   671  
   672  	statusStr := strings.ToUpper(data.Status)
   673  	status := shared.TestStatusValueFromString(statusStr)
   674  	statusStr2 := status.String()
   675  	if statusStr != statusStr2 {
   676  		return fmt.Errorf(`Invalid test status: "%s"`, data.Status)
   677  	}
   678  
   679  	tse.Product = product
   680  	tse.Status = status
   681  
   682  	return nil
   683  }
   684  
   685  // UnmarshalJSON for TestStatusNeq attempts to interpret a query atom as
   686  // {"product": <browser name>, "status": {"not": <status string>}}.
   687  func (tsn *TestStatusNeq) UnmarshalJSON(b []byte) error {
   688  	var data struct {
   689  		BrowserName string `json:"browser_name"` // Legacy
   690  		Product     string `json:"product"`
   691  		Status      struct {
   692  			Not string `json:"not"`
   693  		} `json:"status"`
   694  	}
   695  	if err := json.Unmarshal(b, &data); err != nil {
   696  		return err
   697  	}
   698  	if data.Product == "" && data.BrowserName != "" {
   699  		data.Product = data.BrowserName
   700  	}
   701  	if len(data.Status.Not) == 0 {
   702  		return errors.New(`Missing test status constraint property: "status.not"`)
   703  	}
   704  
   705  	var product *shared.ProductSpec
   706  	if data.Product != "" {
   707  		p, err := shared.ParseProductSpec(data.Product)
   708  		if err != nil {
   709  			return err
   710  		}
   711  		product = &p
   712  	}
   713  
   714  	statusStr := strings.ToUpper(data.Status.Not)
   715  	status := shared.TestStatusValueFromString(statusStr)
   716  	statusStr2 := status.String()
   717  	if statusStr != statusStr2 {
   718  		return fmt.Errorf(`Invalid test status: "%s"`, data.Status)
   719  	}
   720  
   721  	tsn.Product = product
   722  	tsn.Status = status
   723  
   724  	return nil
   725  }
   726  
   727  // UnmarshalJSON for AbstractNot attempts to interpret a query atom as
   728  // {"not": <abstract query>}.
   729  func (n *AbstractNot) UnmarshalJSON(b []byte) error {
   730  	var data struct {
   731  		Not json.RawMessage `json:"not"`
   732  	}
   733  	if err := json.Unmarshal(b, &data); err != nil {
   734  		return err
   735  	}
   736  	if len(data.Not) == 0 {
   737  		return errors.New(`Missing negation property: "not"`)
   738  	}
   739  
   740  	q, err := unmarshalQ(data.Not)
   741  	n.Arg = q
   742  
   743  	return err
   744  }
   745  
   746  // UnmarshalJSON for AbstractOr attempts to interpret a query atom as
   747  // {"or": [<abstract queries>]}.
   748  func (o *AbstractOr) UnmarshalJSON(b []byte) error {
   749  	var data struct {
   750  		Or []json.RawMessage `json:"or"`
   751  	}
   752  	if err := json.Unmarshal(b, &data); err != nil {
   753  		return err
   754  	}
   755  	if len(data.Or) == 0 {
   756  		return errors.New(`Missing disjunction property: "or"`)
   757  	}
   758  
   759  	qs := make([]AbstractQuery, 0, len(data.Or))
   760  	for _, msg := range data.Or {
   761  		q, err := unmarshalQ(msg)
   762  		if err != nil {
   763  			return err
   764  		}
   765  		qs = append(qs, q)
   766  	}
   767  	o.Args = qs
   768  
   769  	return nil
   770  }
   771  
   772  // UnmarshalJSON for AbstractAnd attempts to interpret a query atom as
   773  // {"and": [<abstract queries>]}.
   774  func (a *AbstractAnd) UnmarshalJSON(b []byte) error {
   775  	var data struct {
   776  		And []json.RawMessage `json:"and"`
   777  	}
   778  	if err := json.Unmarshal(b, &data); err != nil {
   779  		return err
   780  	}
   781  	if len(data.And) == 0 {
   782  		return errors.New(`Missing conjunction property: "and"`)
   783  	}
   784  
   785  	qs := make([]AbstractQuery, 0, len(data.And))
   786  	for _, msg := range data.And {
   787  		q, err := unmarshalQ(msg)
   788  		if err != nil {
   789  			return err
   790  		}
   791  		qs = append(qs, q)
   792  	}
   793  	a.Args = qs
   794  
   795  	return nil
   796  }
   797  
   798  // UnmarshalJSON for AbstractExists attempts to interpret a query atom as
   799  // {"exists": [<abstract queries>]}.
   800  func (e *AbstractExists) UnmarshalJSON(b []byte) error {
   801  	var data struct {
   802  		Exists []json.RawMessage `json:"exists"`
   803  	}
   804  	if err := json.Unmarshal(b, &data); err != nil {
   805  		return err
   806  	}
   807  	if len(data.Exists) == 0 {
   808  		return errors.New(`Missing conjunction property: "exists"`)
   809  	}
   810  
   811  	qs := make([]AbstractQuery, 0, len(data.Exists))
   812  	for _, msg := range data.Exists {
   813  		q, err := unmarshalQ(msg)
   814  		if err != nil {
   815  			return err
   816  		}
   817  		qs = append(qs, q)
   818  	}
   819  	e.Args = qs
   820  
   821  	return nil
   822  }
   823  
   824  // UnmarshalJSON for AbstractAll attempts to interpret a query atom as
   825  // {"all": [<abstract query>]}.
   826  func (e *AbstractAll) UnmarshalJSON(b []byte) error {
   827  	var data struct {
   828  		All []json.RawMessage `json:"all"`
   829  	}
   830  	if err := json.Unmarshal(b, &data); err != nil {
   831  		return err
   832  	}
   833  	if len(data.All) == 0 {
   834  		return errors.New(`Missing conjunction property: "all"`)
   835  	}
   836  
   837  	qs := make([]AbstractQuery, 0, len(data.All))
   838  	for _, msg := range data.All {
   839  		q, err := unmarshalQ(msg)
   840  		if err != nil {
   841  			return err
   842  		}
   843  		qs = append(qs, q)
   844  	}
   845  	e.Args = qs
   846  
   847  	return nil
   848  }
   849  
   850  // UnmarshalJSON for AbstractNone attempts to interpret a query atom as
   851  // {"none": [<abstract query>]}.
   852  func (e *AbstractNone) UnmarshalJSON(b []byte) error {
   853  	var data struct {
   854  		None []json.RawMessage `json:"none"`
   855  	}
   856  	if err := json.Unmarshal(b, &data); err != nil {
   857  		return err
   858  	}
   859  	if len(data.None) == 0 {
   860  		return errors.New(`Missing conjunction property: "none"`)
   861  	}
   862  
   863  	qs := make([]AbstractQuery, 0, len(data.None))
   864  	for _, msg := range data.None {
   865  		q, err := unmarshalQ(msg)
   866  		if err != nil {
   867  			return err
   868  		}
   869  		qs = append(qs, q)
   870  	}
   871  	e.Args = qs
   872  
   873  	return nil
   874  }
   875  
   876  // UnmarshalJSON for AbstractSequential attempts to interpret a query atom as
   877  // {"exists": [<abstract queries>]}.
   878  func (e *AbstractSequential) UnmarshalJSON(b []byte) error {
   879  	var data struct {
   880  		Sequential []json.RawMessage `json:"sequential"`
   881  	}
   882  	if err := json.Unmarshal(b, &data); err != nil {
   883  		return err
   884  	}
   885  	if len(data.Sequential) == 0 {
   886  		return errors.New(`Missing conjunction property: "sequential"`)
   887  	}
   888  
   889  	qs := make([]AbstractQuery, 0, len(data.Sequential))
   890  	for _, msg := range data.Sequential {
   891  		q, err := unmarshalQ(msg)
   892  		if err != nil {
   893  			return err
   894  		}
   895  		qs = append(qs, q)
   896  	}
   897  	e.Args = qs
   898  
   899  	return nil
   900  }
   901  
   902  // UnmarshalJSON for AbstractCount attempts to interpret a query atom as
   903  // {"count": int, "where": query}.
   904  func (c *AbstractCount) UnmarshalJSON(b []byte) (err error) {
   905  	var data struct {
   906  		Count json.RawMessage `json:"count"`
   907  		Where json.RawMessage `json:"where"`
   908  	}
   909  	if err := json.Unmarshal(b, &data); err != nil {
   910  		return err
   911  	}
   912  	if len(data.Count) == 0 {
   913  		return errors.New(`Missing count property: "count"`)
   914  	}
   915  	if len(data.Where) == 0 {
   916  		return errors.New(`Missing count property: "where"`)
   917  	}
   918  
   919  	if err := json.Unmarshal(data.Count, &c.Count); err != nil {
   920  		return err
   921  	}
   922  	c.Where, err = unmarshalQ(data.Where)
   923  	if err != nil {
   924  		return err
   925  	}
   926  
   927  	return nil
   928  }
   929  
   930  // UnmarshalJSON for AbstractLessThan attempts to interpret a query atom as
   931  // {"count": int, "where": query}.
   932  func (l *AbstractLessThan) UnmarshalJSON(b []byte) error {
   933  	var data struct {
   934  		Count json.RawMessage `json:"lessThan"`
   935  		Where json.RawMessage `json:"where"`
   936  	}
   937  	if err := json.Unmarshal(b, &data); err != nil {
   938  		return err
   939  	}
   940  	if len(data.Count) == 0 {
   941  		return errors.New(`Missing lessThan property: "lessThan"`)
   942  	}
   943  	if len(data.Where) == 0 {
   944  		return errors.New(`Missing count property: "where"`)
   945  	}
   946  
   947  	err := json.Unmarshal(data.Count, &l.Count)
   948  	if err != nil {
   949  		return err
   950  	}
   951  	l.Where, err = unmarshalQ(data.Where)
   952  	if err != nil {
   953  		return err
   954  	}
   955  
   956  	return nil
   957  }
   958  
   959  // UnmarshalJSON for AbstractMoreThan attempts to interpret a query atom as
   960  // {"count": int, "where": query}.
   961  func (m *AbstractMoreThan) UnmarshalJSON(b []byte) (err error) {
   962  	var data struct {
   963  		Count json.RawMessage `json:"moreThan"`
   964  		Where json.RawMessage `json:"where"`
   965  	}
   966  	if err := json.Unmarshal(b, &data); err != nil {
   967  		return err
   968  	}
   969  	if len(data.Count) == 0 {
   970  		return errors.New(`Missing moreThan property: "moreThan"`)
   971  	}
   972  	if len(data.Where) == 0 {
   973  		return errors.New(`Missing count property: "where"`)
   974  	}
   975  
   976  	if err := json.Unmarshal(data.Count, &m.Count); err != nil {
   977  		return err
   978  	}
   979  	m.Where, err = unmarshalQ(data.Where)
   980  	if err != nil {
   981  		return err
   982  	}
   983  
   984  	return nil
   985  }
   986  
   987  // UnmarshalJSON for AbstractLink attempts to interpret a query atom as
   988  // {"link":<metadata url pattern string>}.
   989  func (l *AbstractLink) UnmarshalJSON(b []byte) error {
   990  	var data map[string]*json.RawMessage
   991  	if err := json.Unmarshal(b, &data); err != nil {
   992  		return err
   993  	}
   994  	patternMsg, ok := data["link"]
   995  	if !ok {
   996  		return errors.New(`Missing Link pattern property: "link"`)
   997  	}
   998  	var pattern string
   999  	if err := json.Unmarshal(*patternMsg, &pattern); err != nil {
  1000  		return errors.New(`Missing link pattern property "pattern" is not a string`)
  1001  	}
  1002  
  1003  	l.Pattern = pattern
  1004  
  1005  	return nil
  1006  }
  1007  
  1008  // UnmarshalJSON for AbstractTestLabel attempts to interpret a query atom as
  1009  // {"label":<label string>}.
  1010  func (t *AbstractTestLabel) UnmarshalJSON(b []byte) error {
  1011  	var data map[string]*json.RawMessage
  1012  	if err := json.Unmarshal(b, &data); err != nil {
  1013  		return err
  1014  	}
  1015  	labelMsg, ok := data["label"]
  1016  	if !ok {
  1017  		return errors.New(`Missing label pattern property: "label"`)
  1018  	}
  1019  	var label string
  1020  	if err := json.Unmarshal(*labelMsg, &label); err != nil {
  1021  		return errors.New(`Property "label" is not a string`)
  1022  	}
  1023  
  1024  	t.Label = label
  1025  
  1026  	return nil
  1027  }
  1028  
  1029  // TestWebFeatureAtom contains the parsed data from a "feature" query atom.
  1030  type TestWebFeatureAtom struct {
  1031  	WebFeature string
  1032  }
  1033  
  1034  // UnmarshalJSON for TestWebFeatureAtom attempts to interpret a query atom as
  1035  // {"feature":<web_feature_string>}.
  1036  func (t *TestWebFeatureAtom) UnmarshalJSON(b []byte) error {
  1037  	var data map[string]*json.RawMessage
  1038  	if err := json.Unmarshal(b, &data); err != nil {
  1039  		return err
  1040  	}
  1041  	webFeatureMsg, ok := data["feature"]
  1042  	if !ok {
  1043  		return errors.New(`Missing web feature pattern property: "feature"`)
  1044  	}
  1045  	var webFeature string
  1046  	if err := json.Unmarshal(*webFeatureMsg, &webFeature); err != nil {
  1047  		return errors.New(`Property "feature" is not a string`)
  1048  	}
  1049  
  1050  	t.WebFeature = webFeature
  1051  
  1052  	return nil
  1053  }
  1054  
  1055  // UnmarshalJSON for AbstractTriaged attempts to interpret a query atom as
  1056  // {"triaged":<browser name>}.
  1057  func (t *AbstractTriaged) UnmarshalJSON(b []byte) error {
  1058  	var data map[string]*json.RawMessage
  1059  	if err := json.Unmarshal(b, &data); err != nil {
  1060  		return err
  1061  	}
  1062  
  1063  	browserNameMsg, ok := data["triaged"]
  1064  	if !ok {
  1065  		return errors.New(`Missing Triaged property: "triaged"`)
  1066  	}
  1067  
  1068  	var browserName string
  1069  	if err := json.Unmarshal(*browserNameMsg, &browserName); err != nil {
  1070  		return errors.New(`Triaged property "triaged" is not a string`)
  1071  	}
  1072  
  1073  	var product *shared.ProductSpec
  1074  	if browserName != "" {
  1075  		p, err := shared.ParseProductSpec(browserName)
  1076  		if err != nil {
  1077  			return err
  1078  		}
  1079  		product = &p
  1080  	}
  1081  
  1082  	t.Product = product
  1083  
  1084  	return nil
  1085  }
  1086  
  1087  // UnmarshalJSON for MetadataQuality attempts to interpret a query atom as
  1088  // {"is":<metadata quality>}.
  1089  func (q *MetadataQuality) UnmarshalJSON(b []byte) (err error) {
  1090  	var data map[string]*json.RawMessage
  1091  	if err := json.Unmarshal(b, &data); err != nil {
  1092  		return err
  1093  	}
  1094  	is, ok := data["is"]
  1095  	if !ok {
  1096  		return errors.New(`Missing "is" pattern property: "is"`)
  1097  	}
  1098  	var quality string
  1099  	if err := json.Unmarshal(*is, &quality); err != nil {
  1100  		return errors.New(`"is" property is not a string`)
  1101  	}
  1102  
  1103  	*q, err = MetadataQualityFromString(quality)
  1104  
  1105  	return err
  1106  }
  1107  
  1108  // MetadataQualityFromString returns the enum value for the given string.
  1109  func MetadataQualityFromString(quality string) (MetadataQuality, error) {
  1110  	switch quality {
  1111  	case "different":
  1112  		return MetadataQualityDifferent, nil
  1113  	case "tentative":
  1114  		return MetadataQualityTentative, nil
  1115  	case "optional":
  1116  		return MetadataQualityOptional, nil
  1117  	}
  1118  
  1119  	return MetadataQualityUnknown, fmt.Errorf(`Unknown "is" quality "%s"`, quality)
  1120  }
  1121  
  1122  // nolint:ireturn // TODO: Fix ireturn lint error
  1123  func unmarshalQ(b []byte) (AbstractQuery, error) {
  1124  	{
  1125  		var tnp TestNamePattern
  1126  		if err := json.Unmarshal(b, &tnp); err == nil {
  1127  			return tnp, nil
  1128  		}
  1129  	}
  1130  	{
  1131  		var stnp SubtestNamePattern
  1132  		if err := json.Unmarshal(b, &stnp); err == nil {
  1133  			return stnp, nil
  1134  		}
  1135  	}
  1136  	{
  1137  		var tp TestPath
  1138  		if err := json.Unmarshal(b, &tp); err == nil {
  1139  			return tp, nil
  1140  		}
  1141  	}
  1142  	{
  1143  		var tse TestStatusEq
  1144  		if err := json.Unmarshal(b, &tse); err == nil {
  1145  			return tse, nil
  1146  		}
  1147  	}
  1148  	{
  1149  		var tsn TestStatusNeq
  1150  		if err := json.Unmarshal(b, &tsn); err == nil {
  1151  			return tsn, nil
  1152  		}
  1153  	}
  1154  	{
  1155  		var n AbstractNot
  1156  		if err := json.Unmarshal(b, &n); err == nil {
  1157  			return n, nil
  1158  		}
  1159  	}
  1160  	{
  1161  		var o AbstractOr
  1162  		if err := json.Unmarshal(b, &o); err == nil {
  1163  			return o, nil
  1164  		}
  1165  	}
  1166  	{
  1167  		var a AbstractAnd
  1168  		if err := json.Unmarshal(b, &a); err == nil {
  1169  			return a, nil
  1170  		}
  1171  	}
  1172  	{
  1173  		var e AbstractExists
  1174  		if err := json.Unmarshal(b, &e); err == nil {
  1175  			return e, nil
  1176  		}
  1177  	}
  1178  	{
  1179  		var a AbstractAll
  1180  		if err := json.Unmarshal(b, &a); err == nil {
  1181  			return a, nil
  1182  		}
  1183  	}
  1184  	{
  1185  		var n AbstractNone
  1186  		if err := json.Unmarshal(b, &n); err == nil {
  1187  			return n, nil
  1188  		}
  1189  	}
  1190  	{
  1191  		var s AbstractSequential
  1192  		if err := json.Unmarshal(b, &s); err == nil {
  1193  			return s, nil
  1194  		}
  1195  	}
  1196  	{
  1197  		var c AbstractCount
  1198  		if err := json.Unmarshal(b, &c); err == nil {
  1199  			return c, nil
  1200  		}
  1201  	}
  1202  	{
  1203  		var c AbstractLessThan
  1204  		if err := json.Unmarshal(b, &c); err == nil {
  1205  			return c, nil
  1206  		}
  1207  	}
  1208  	{
  1209  		var c AbstractMoreThan
  1210  		if err := json.Unmarshal(b, &c); err == nil {
  1211  			return c, nil
  1212  		}
  1213  	}
  1214  	{
  1215  		var l AbstractLink
  1216  		if err := json.Unmarshal(b, &l); err == nil {
  1217  			return l, nil
  1218  		}
  1219  	}
  1220  	{
  1221  		var i MetadataQuality
  1222  		if err := json.Unmarshal(b, &i); err == nil {
  1223  			return i, nil
  1224  		}
  1225  	}
  1226  	{
  1227  		var t AbstractTriaged
  1228  		if err := json.Unmarshal(b, &t); err == nil {
  1229  			return t, nil
  1230  		}
  1231  	}
  1232  	{
  1233  		var t AbstractTestLabel
  1234  		if err := json.Unmarshal(b, &t); err == nil {
  1235  			return t, nil
  1236  		}
  1237  	}
  1238  	{
  1239  		var atom TestWebFeatureAtom
  1240  		if err := json.Unmarshal(b, &atom); err == nil {
  1241  			return AbstractTestWebFeature{
  1242  				TestWebFeatureAtom: atom,
  1243  				manifestFetcher:    searchcacheWebFeaturesManifestFetcher{},
  1244  			}, nil
  1245  		}
  1246  	}
  1247  	const docsFilePath = "wpt.fyi/api/query/README.md"
  1248  	errorMsg := fmt.Sprintf("Failed to parse query fragment as any of the existing search atoms in %s", docsFilePath)
  1249  
  1250  	return nil, errors.New(errorMsg)
  1251  }