// Copyright 2025 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package promqltest import ( "fmt" "os" "path/filepath" "strings" "github.com/grafana/regexp" ) const defaultTestDataDir = "promql/promqltest/testdata" var ( evalRegex = regexp.MustCompile(`^(eval |eval_fail |eval_warn |eval_info |eval_ordered )(.*)$`) indentRegex = regexp.MustCompile(`^([ \t]+)\S`) ) type MigrateMode int const ( MigrateStrict MigrateMode = iota MigrateBasic MigrateTolerant ) func ParseMigrateMode(s string) (MigrateMode, error) { switch s { case "strict": return MigrateStrict, nil case "basic": return MigrateBasic, nil case "tolerant": return MigrateTolerant, nil default: return MigrateStrict, fmt.Errorf("invalid mode: %s", s) } } // MigrateTestData migrates all PromQL test files to the new syntax format. // It applies annotation rules based on the provided migration mode ("strict", "basic", or "tolerant"). // The function parses each .test file, converts it to the new syntax and overwrites the file. func MigrateTestData(mode, dir string) error { if dir == "" { dir = defaultTestDataDir } migrationMode, err := ParseMigrateMode(mode) if err != nil { return fmt.Errorf("failed to parse mode: %w", err) } files, err := os.ReadDir(dir) if err != nil { return fmt.Errorf("failed to read testdata directory: %w", err) } annotationMap := map[MigrateMode]map[string][]string{ MigrateStrict: { "eval_fail": {"expect fail", "expect no_warn", "expect no_info"}, "eval_warn": {"expect warn", "expect no_info"}, "eval_info": {"expect info", "expect no_warn"}, "eval_ordered": {"expect ordered", "expect no_warn", "expect no_info"}, "eval": {"expect no_warn", "expect no_info"}, }, MigrateBasic: { "eval_fail": {"expect fail"}, "eval_warn": {"expect warn"}, "eval_info": {"expect info"}, "eval_ordered": {"expect ordered"}, }, MigrateTolerant: { "eval_fail": {"expect fail"}, "eval_ordered": {"expect ordered"}, }, } for _, file := range files { if file.IsDir() || !strings.HasSuffix(file.Name(), ".test") { continue } path := filepath.Join(dir, file.Name()) content, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read file %s: %w", path, err) } lines := strings.Split(string(content), "\n") processedLines, err := processTestFileLines(lines, annotationMap[migrationMode], evalRegex) if err != nil { return fmt.Errorf("error processing file %s: %w", path, err) } if err := os.WriteFile(path, []byte(strings.Join(processedLines, "\n")), 0o644); err != nil { return fmt.Errorf("failed to write file %s: %w", path, err) } } return nil } func processTestFileLines( lines []string, annotationMap map[string][]string, evalRegex *regexp.Regexp, ) (result []string, err error) { for i := 0; i < len(lines); i++ { startLine := lines[i] matches := evalRegex.FindStringSubmatch(strings.TrimSpace(startLine)) if matches == nil { result = append(result, startLine) continue } var inputBlock []string var outputBlock []string skipBlock := false i++ for i < len(lines) { inputBlock = append(inputBlock, lines[i]) if strings.HasPrefix(strings.TrimSpace(lines[i]), "expect ") { skipBlock = true } if i+1 < len(lines) && evalRegex.MatchString(strings.TrimSpace(lines[i+1])) { break } i++ } if skipBlock { result = append(result, startLine) result = append(result, inputBlock...) continue } // Get leading whitespace from startLine using indentRegex. leadingWS := "" if indentMatch := indentRegex.FindStringSubmatch(startLine); indentMatch != nil { leadingWS = indentMatch[1] } command := strings.TrimSpace(matches[1]) expression := matches[2] var annotations []string result = append(result, leadingWS+fmt.Sprintf("eval %s", expression)) // Detecting indentation style (tab or space) from the first non-empty, indented line. indent := " " for _, line := range inputBlock { if indentMatch := indentRegex.FindStringSubmatch(line); indentMatch != nil { indent = indentMatch[1] break } } for _, annotation := range annotationMap[command] { annotations = append(annotations, indent+annotation) } for _, line := range inputBlock { trimmedLine := strings.TrimSpace(line) switch { case strings.HasPrefix(trimmedLine, "expected_fail_message"): msg := strings.TrimPrefix(trimmedLine, "expected_fail_message ") for j, s := range annotations { if strings.Contains(s, "expect fail") { annotations[j] = indent + fmt.Sprintf("expect fail msg:%s", msg) } } case strings.HasPrefix(trimmedLine, "expected_fail_regexp"): regex := strings.TrimPrefix(trimmedLine, "expected_fail_regexp ") for j, s := range annotations { if strings.Contains(s, "expect fail") { annotations[j] = indent + fmt.Sprintf("expect fail regex:%s", regex) } } default: outputBlock = append(outputBlock, line) } } result = append(result, annotations...) result = append(result, outputBlock...) } return result, nil }