Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add windows integration test #1912

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/integration-tests-win.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Integration Tests Windows
on:
# Run tests on main just so the module and build cache can be saved and used
# in PRs. This speeds up the time it takes to test PRs dramatically.
# (More information on https://docs.github.com/en/[email protected]/actions/using-workflows/caching-dependencies-to-speed-up-workflows)
push:
branches:
- main
pull_request:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this enable workflow for pull requests? is this temporary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I did not change it since our discussion, I will remove it before merging to make sure that the test is running properly

jobs:
run_tests:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that become another place where we need to update Go version? What's the process currently to update these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, and afaik it's just "search and replace" through the codebase :/

- name: Run tests
run: make integration-test
1 change: 0 additions & 1 deletion internal/cmd/integration-tests/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "3"
services:

mimir:
Expand Down
28 changes: 20 additions & 8 deletions internal/cmd/integration-tests/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/spf13/cobra"
Expand All @@ -30,26 +31,37 @@ func main() {

func runIntegrationTests(cmd *cobra.Command, args []string) {
defer reportResults()
defer cleanUpEnvironment()

testFolder := "./tests/"
alloyBinaryPath := "../../../../../build/alloy"
alloyBinary := "build/alloy"
Comment on lines +35 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's switch to using https://pkg.go.dev/path#Join


if runtime.GOOS == "windows" {
testFolder = "./tests-windows/"
alloyBinaryPath = "..\\..\\..\\..\\..\\build\\alloy.exe"
alloyBinary = "build/alloy.exe"
} else {
setupEnvironment()
defer cleanUpEnvironment()
Comment on lines +44 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no setup or clean up on windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't use the linux images in the windows runner so we are quite limited in terms of environment. For now not using docker and only running one Alloy to run the windows exporter seems to be sufficient. I can rename the function to "setupDockerEnv" and "cleanupDockerEnv" to make it more clear

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if for clarity we should have two types of tests:

  • one running docker compose with dependency projects - maybe "docker-integration-test"
  • another one with just Alloy - maybe "standalone-integration-test"

And have them somewhat more clearly separated in the code. Otherwise it becomes a bit convoluted 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that it would be really worth it because besides the docker part which is quite small, they would re-use everything. Also I don't think that we need a linux standalone. And maybe one day the windows runner will be able to use WSL 2 and it will work with Mimir linux image or at some point we may want to run a docker windows image and the difference will be gone.
I agree that now it looks a bit convoluted though. I can structure the test better so that it looks less chaotic

}

if !skipBuild {
buildAlloy()
buildAlloy(alloyBinary)
}
setupEnvironment()

if specificTest != "" {
fmt.Println("Running", specificTest)
if !filepath.IsAbs(specificTest) && !strings.HasPrefix(specificTest, "./tests/") {
specificTest = "./tests/" + specificTest
if !filepath.IsAbs(specificTest) && !strings.HasPrefix(specificTest, testFolder) {
specificTest = testFolder + specificTest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
logChan = make(chan TestLog, 1)
runSingleTest(specificTest, 12345)
runSingleTest(alloyBinaryPath, specificTest, 12345)
} else {
testDirs, err := filepath.Glob("./tests/*")
testDirs, err := filepath.Glob(testFolder + "*")
if err != nil {
panic(err)
}
logChan = make(chan TestLog, len(testDirs))
runAllTests()
runAllTests(alloyBinaryPath, testFolder)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
prometheus.exporter.windows "default" { }

prometheus.scrape "default" {
targets = prometheus.exporter.windows.default.targets
forward_to = [prometheus.remote_write.default.receiver]
scrape_interval = "1s"
scrape_timeout = "500ms"
}

prometheus.remote_write "default" {
endpoint {
url = "http://localhost:9090/receive"
metadata_config {
send_interval = "1s"
}
queue_config {
max_samples_per_send = 100
}
}
external_labels = {
test_name = "win_metrics",
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//go:build windows

package main

import (
"context"
"fmt"
"io"
"net"
"net/http"
"strings"
"testing"
"time"

"github.com/golang/snappy"
"github.com/prometheus/prometheus/prompb"
"github.com/stretchr/testify/require"
)

// List of expected Windows metrics
var winMetrics = []string{
"windows_cpu_time_total", // cpu
"windows_cs_logical_processors", // cs
"windows_logical_disk_info", // logical_disk
"windows_net_bytes_received_total", // net
"windows_os_info", // os
"windows_service_info", // service
"windows_system_system_up_time", // system
}

// TestWindowsMetrics sets up a server to receive remote write requests
// and checks if required metrics appear within a one minute timeout
func TestWindowsMetrics(t *testing.T) {
foundMetrics := make(map[string]bool)
for _, metric := range winMetrics {
foundMetrics[metric] = false
}

done := make(chan bool)
srv := &http.Server{Addr: ":9090"}
http.HandleFunc("/receive", func(w http.ResponseWriter, r *http.Request) {
ts, _, err := handlePost(t, w, r)

if err != nil {
t.Log("Cancel processing request.")
return
}

for _, timeseries := range ts {
var metricName string
for _, label := range timeseries.Labels {
if label.Name == "__name__" {
metricName = label.Value
break
}
}
for _, requiredMetric := range winMetrics {
if requiredMetric == metricName && !foundMetrics[requiredMetric] {
foundMetrics[requiredMetric] = true
}
}
}

allFound := true
for _, found := range foundMetrics {
if !found {
allFound = false
break
}
}

if allFound {
done <- true
}
})

go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic(fmt.Errorf("could not start server: %v", err))
}
}()
defer srv.Shutdown(context.Background())

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

select {
case <-ctx.Done():
missingMetrics := []string{}
for metric, found := range foundMetrics {
if !found {
missingMetrics = append(missingMetrics, metric)
}
}
if len(missingMetrics) > 0 {
t.Errorf("Timeout reached. Missing metrics: %v", missingMetrics)
} else {
t.Log("All required metrics received.")
}
case <-done:
t.Log("All required metrics received within the timeout.")
}
}

func handlePost(t *testing.T, _ http.ResponseWriter, r *http.Request) ([]prompb.TimeSeries, []prompb.MetricMetadata, error) {
defer r.Body.Close()
data, err := io.ReadAll(r.Body)

// ignore this error because the server might shutdown while a request is being processed
if opErr, ok := err.(*net.OpError); ok && strings.Contains(opErr.Err.Error(), "use of closed network connection") {
return nil, nil, err
}

require.NoError(t, err)

data, err = snappy.Decode(nil, data)
require.NoError(t, err)

var req prompb.WriteRequest
err = req.Unmarshal(data)
require.NoError(t, err)
return req.GetTimeseries(), req.Metadata, nil
}
19 changes: 9 additions & 10 deletions internal/cmd/integration-tests/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ import (
"time"
)

const (
alloyBinaryPath = "../../../../../build/alloy"
)

type TestLog struct {
TestDir string
AlloyLog string
Expand All @@ -34,8 +30,8 @@ func executeCommand(command string, args []string, taskDescription string) {
}
}

func buildAlloy() {
executeCommand("make", []string{"-C", "../../..", "alloy"}, "Building Alloy")
func buildAlloy(alloyBinary string) {
executeCommand("make", []string{"-C", "../../..", "alloy", fmt.Sprintf("ALLOY_BINARY=%s", alloyBinary)}, "Building Alloy")
}

func setupEnvironment() {
Expand All @@ -44,7 +40,7 @@ func setupEnvironment() {
time.Sleep(45 * time.Second)
}

func runSingleTest(testDir string, port int) {
func runSingleTest(alloyBinaryPath string, testDir string, port int) {
info, err := os.Stat(testDir)
if err != nil {
panic(err)
Expand Down Expand Up @@ -88,14 +84,17 @@ func runSingleTest(testDir string, port int) {
}
}

// sleep for a few seconds before deleting the files to make sure that they are not use anymore
time.Sleep(5 * time.Second)

Comment on lines +87 to +89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of sleeps, can we have a retry and backoff (without panic on first failure) to remove all files?
Sleeps are notoriously fragile and should be avoided. A slow machine can sometimes hang for 5 seconds.

err = os.RemoveAll(filepath.Join(testDir, "data-alloy"))
if err != nil {
panic(err)
}
}

func runAllTests() {
testDirs, err := filepath.Glob("./tests/*")
func runAllTests(alloyBinaryPath string, testFolder string) {
testDirs, err := filepath.Glob(testFolder + "*")
if err != nil {
panic(err)
}
Expand All @@ -106,7 +105,7 @@ func runAllTests() {
wg.Add(1)
go func(td string, offset int) {
defer wg.Done()
runSingleTest(td, port+offset)
runSingleTest(alloyBinaryPath, td, port+offset)
}(testDir, i)
}
wg.Wait()
Expand Down