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

     1  // Copyright 2019 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 shared
     6  
     7  import (
     8  	"strings"
     9  
    10  	"gopkg.in/yaml.v3"
    11  )
    12  
    13  // ShowMetadataParam determines whether Metadata Information returns along
    14  // with a test result query request.
    15  const ShowMetadataParam = "metadataInfo"
    16  
    17  // MetadataFileName is the name of Metadata files in the wpt-metadata repo.
    18  const MetadataFileName = "META.yml"
    19  
    20  // MetadataResults is a map from test paths to all of the links under that test path.
    21  // It represents a flattened copy of the wpt-metadata repository, which has metadata
    22  // sharded across as large number of files in a directory structure.
    23  type MetadataResults map[string]MetadataLinks
    24  
    25  // Metadata represents a wpt-metadata META.yml file.
    26  type Metadata struct {
    27  	Links MetadataLinks `yaml:"links"`
    28  }
    29  
    30  // MetadataLinks is a helper type for a MetadataLink slice.
    31  type MetadataLinks []MetadataLink
    32  
    33  // MetadataLink is an item in the `links` node of a wpt-metadata
    34  // META.yml file, which lists an external reference, optionally
    35  // filtered by product and a specific test.
    36  type MetadataLink struct {
    37  	Product ProductSpec          `yaml:"product,omitempty" json:"product,omitempty"`
    38  	URL     string               `yaml:"url,omitempty"     json:"url"`
    39  	Label   string               `yaml:"label,omitempty"  json:"label,omitempty"`
    40  	Results []MetadataTestResult `yaml:"results" json:"results,omitempty"`
    41  }
    42  
    43  // MetadataTestResult is a filter for test results to which the Metadata link
    44  // should apply.
    45  type MetadataTestResult struct {
    46  	TestPath    string      `yaml:"test"    json:"test,omitempty"`
    47  	SubtestName *string     `yaml:"subtest,omitempty" json:"subtest,omitempty"`
    48  	Status      *TestStatus `yaml:"status,omitempty"  json:"status,omitempty"`
    49  }
    50  
    51  // GetMetadataResponse retrieves the response to a WPT Metadata query. Metadata
    52  // is included for any product that matches a passed TestRun. Test-level
    53  // metadata (i.e. that is not associated with any product) may be fetched by
    54  // passing true for includeTestLevel.
    55  func GetMetadataResponse(testRuns []TestRun, includeTestLevel bool, log Logger, fetcher MetadataFetcher) (MetadataResults, error) {
    56  	var productSpecs = make([]ProductSpec, len(testRuns))
    57  	for i, run := range testRuns {
    58  		productSpecs[i] = ProductSpec{ProductAtRevision: run.ProductAtRevision, Labels: run.LabelsSet()}
    59  	}
    60  
    61  	// TODO(kyleju): Include the SHA information in API response;
    62  	// see https://github.com/web-platform-tests/wpt.fyi/issues/1938
    63  	_, metadata, err := GetMetadataByteMap(log, fetcher)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	return constructMetadataResponse(productSpecs, includeTestLevel, metadata), nil
    69  }
    70  
    71  // GetMetadataResponseOnProducts constructs the response to a WPT Metadata
    72  // query, given ProductSpecs. Metdata is included for any product that matches
    73  // a passed ProductSpec. Test-level metadata (i.e. that is not associated with
    74  // any product) may be fetched by passing true for includeTestLevel.
    75  func GetMetadataResponseOnProducts(productSpecs ProductSpecs, includeTestLevel bool, log Logger, fetcher MetadataFetcher) (MetadataResults, error) {
    76  	// TODO(kyleju): Include the SHA information in API response;
    77  	// see https://github.com/web-platform-tests/wpt.fyi/issues/1938
    78  	_, metadata, err := GetMetadataByteMap(log, fetcher)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	return constructMetadataResponse(productSpecs, includeTestLevel, metadata), nil
    84  }
    85  
    86  // GetMetadataByteMap collects and parses all META.yml files from
    87  // the wpt-metadata repository.
    88  func GetMetadataByteMap(log Logger, fetcher MetadataFetcher) (sha *string, metadata map[string]Metadata, err error) {
    89  	sha, metadataByteMap, err := fetcher.Fetch()
    90  	if err != nil {
    91  		log.Errorf("Error from FetchMetadata: %s", err.Error())
    92  		return nil, nil, err
    93  	}
    94  
    95  	metadata = parseMetadata(metadataByteMap, log)
    96  	return sha, metadata, nil
    97  }
    98  
    99  func parseMetadata(metadataByteMap map[string][]byte, log Logger) map[string]Metadata {
   100  	var metadataMap = make(map[string]Metadata)
   101  	for path, data := range metadataByteMap {
   102  		var metadata Metadata
   103  		err := yaml.Unmarshal(data, &metadata)
   104  		if err != nil {
   105  			log.Warningf("Failed to unmarshal %s.", path)
   106  			continue
   107  		}
   108  		metadataMap[path] = metadata
   109  	}
   110  	return metadataMap
   111  }
   112  
   113  // addResponseLink is a helper method for constructMetadataResponse. It creates a new MetadataLink
   114  // object corresponding to a specific MetadataTestResult for a given test, and adds it to a
   115  // MetadataResults map.
   116  func addResponseLink(fullTestName string, link MetadataLink, result MetadataTestResult, outMap MetadataResults) {
   117  	newLink := MetadataLink{
   118  		Product: link.Product,
   119  		URL:     link.URL,
   120  		Label:   link.Label,
   121  	}
   122  	if result.SubtestName != nil || result.Status != nil {
   123  		newLink.Results = []MetadataTestResult{
   124  			{
   125  				SubtestName: result.SubtestName,
   126  				Status:      result.Status,
   127  				// TestPath is redundant (it's the map key in outMap)
   128  			},
   129  		}
   130  	}
   131  	if _, ok := outMap[fullTestName]; !ok {
   132  		outMap[fullTestName] = MetadataLinks{newLink}
   133  	} else {
   134  		outMap[fullTestName] = append(outMap[fullTestName], newLink)
   135  	}
   136  }
   137  
   138  // constructMetadataResponse constructs the response to a WPT Metadata query, given ProductSpecs.
   139  func constructMetadataResponse(productSpecs ProductSpecs, includeTestLevel bool, metadata map[string]Metadata) MetadataResults {
   140  	res := make(MetadataResults)
   141  	for folderPath, data := range metadata {
   142  		for i := range data.Links {
   143  			link := data.Links[i]
   144  			for _, result := range link.Results {
   145  				//TODO(kyleju): Concatenate test path on WPT Metadata repository instead of here.
   146  				fullTestName := GetWPTTestPath(folderPath, result.TestPath)
   147  				if link.Product.BrowserName == "" && includeTestLevel {
   148  					addResponseLink(fullTestName, link, result, res)
   149  				}
   150  
   151  				// Find any matching product for this link result (there can be at most one).
   152  				for _, productSpec := range productSpecs {
   153  					// Matches on browser type if a version is not specified.
   154  					if link.Product.MatchesProductSpec(productSpec) {
   155  						addResponseLink(fullTestName, link, result, res)
   156  						break
   157  					}
   158  				}
   159  			}
   160  		}
   161  	}
   162  	return res
   163  }
   164  
   165  // PrepareLinkFilter maps a MetadataResult test name to its URLs.
   166  func PrepareLinkFilter(metadata MetadataResults) map[string][]string {
   167  	metadataMap := make(map[string][]string)
   168  	for test, links := range metadata {
   169  		for _, link := range links {
   170  			if link.URL == "" {
   171  				continue
   172  			}
   173  			if urls, ok := metadataMap[test]; !ok {
   174  				metadataMap[test] = []string{link.URL}
   175  			} else {
   176  				metadataMap[test] = append(urls, link.URL)
   177  			}
   178  		}
   179  	}
   180  	return metadataMap
   181  }
   182  
   183  // PrepareTestLabelFilter maps a MetadataResult test name to its labels.
   184  func PrepareTestLabelFilter(metadata MetadataResults) map[string][]string {
   185  	metadataMap := make(map[string][]string)
   186  	for test, links := range metadata {
   187  		for _, link := range links {
   188  			if link.Label == "" {
   189  				continue
   190  			}
   191  			if labels, ok := metadataMap[test]; !ok {
   192  				metadataMap[test] = []string{link.Label}
   193  			} else {
   194  				metadataMap[test] = append(labels, link.Label)
   195  			}
   196  
   197  		}
   198  	}
   199  	return metadataMap
   200  }
   201  
   202  // GetWPTTestPath concatenates a folder path and a test name into a WPT test path.
   203  func GetWPTTestPath(folderPath string, testname string) string {
   204  	if folderPath == "" {
   205  		return "/" + testname
   206  	}
   207  	return "/" + folderPath + "/" + testname
   208  }
   209  
   210  // SplitWPTTestPath splits a WPT test path into a folder path and a test name.
   211  func SplitWPTTestPath(githubPath string) (string, string) {
   212  	if !strings.HasPrefix(githubPath, "/") {
   213  		return "", ""
   214  	}
   215  
   216  	pathArray := strings.Split(githubPath, "/")[1:]
   217  	if len(pathArray) == 1 {
   218  		return "", pathArray[0]
   219  	}
   220  
   221  	folderPath := strings.Join(pathArray[:len(pathArray)-1], "/")
   222  	testName := pathArray[len(pathArray)-1]
   223  	return folderPath, testName
   224  }
   225  
   226  // GetMetadataFilePath appends MetadataFileName to a Metadata folder path.
   227  func GetMetadataFilePath(folderName string) string {
   228  	if folderName == "" {
   229  		return MetadataFileName
   230  	}
   231  
   232  	return folderName + "/" + MetadataFileName
   233  }