// Copyright 2020 The gVisor 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 fio_test

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"gvisor.dev/gvisor/pkg/cleanup"
	"gvisor.dev/gvisor/pkg/test/dockerutil"
	"gvisor.dev/gvisor/test/benchmarks/harness"
	"gvisor.dev/gvisor/test/benchmarks/tools"
)

// BenchmarkFio runs fio on the runtime under test. There are 4 basic test
// cases each run on a tmpfs mount and a bind mount. Fio requires root so that
// caches can be dropped.
func BenchmarkFio(b *testing.B) {
	testCases := []tools.Fio{
		{
			Test:      "write",
			BlockSize: 4,
			IODepth:   4,
		},
		{
			Test:      "write",
			BlockSize: 1024,
			IODepth:   4,
		},
		{
			Test:      "read",
			BlockSize: 4,
			IODepth:   4,
		},
		{
			Test:      "read",
			BlockSize: 1024,
			IODepth:   4,
		},
		{
			Test:      "randwrite",
			BlockSize: 4,
			IODepth:   4,
		},
		{
			Test:      "randread",
			BlockSize: 4,
			IODepth:   4,
		},
	}

	machine, err := harness.GetMachine()
	if err != nil {
		b.Fatalf("failed to get machine with: %v", err)
	}
	defer machine.CleanUp()

	for _, fsType := range []harness.FileSystemType{harness.BindFS, harness.TmpFS, harness.RootFS} {
		for _, tc := range testCases {
			operation := tools.Parameter{
				Name:  "operation",
				Value: tc.Test,
			}
			blockSize := tools.Parameter{
				Name:  "blockSize",
				Value: fmt.Sprintf("%dK", tc.BlockSize),
			}
			filesystem := tools.Parameter{
				Name:  "filesystem",
				Value: string(fsType),
			}
			name, err := tools.ParametersToName(operation, blockSize, filesystem)
			if err != nil {
				b.Fatalf("Failed to parser paramters: %v", err)
			}
			b.Run(name, func(b *testing.B) {
				b.StopTimer()
				tc.Size = b.N

				ctx := context.Background()
				container := machine.GetContainer(ctx, b)
				cu := cleanup.Make(func() {
					container.CleanUp(ctx)
				})
				defer cu.Clean()

				mnts, outdir, err := harness.MakeMount(machine, fsType, &cu)
				if err != nil {
					b.Fatalf("failed to make mount: %v", err)
				}

				// Start the container with the mount.
				if err := container.Spawn(
					ctx, dockerutil.RunOpts{
						Image:  "benchmarks/fio",
						Mounts: mnts,
					},
					// Sleep on the order of b.N.
					"sleep", fmt.Sprintf("%d", 1000*b.N),
				); err != nil {
					b.Fatalf("failed to start fio container with: %v", err)
				}

				if out, err := container.Exec(ctx, dockerutil.ExecOpts{},
					"mkdir", "-p", outdir); err != nil {
					b.Fatalf("failed to copy directory: %v (%s)", err, out)
				}

				// Directory and filename inside container where fio will read/write.
				outfile := filepath.Join(outdir, "test.txt")

				// For reads, we need a file to read so make one inside the container.
				if strings.Contains(tc.Test, "read") {
					fallocateCmd := fmt.Sprintf("fallocate -l %dK %s", tc.Size, outfile)
					if out, err := container.Exec(ctx, dockerutil.ExecOpts{},
						strings.Split(fallocateCmd, " ")...); err != nil {
						b.Fatalf("failed to create readable file on mount: %v, %s", err, out)
					}
				}

				// Drop caches just before running.
				if err := harness.DropCaches(machine); err != nil {
					b.Skipf("failed to drop caches with %v. You probably need root.", err)
				}

				cmd := tc.MakeCmd(outfile)
				if err := harness.DropCaches(machine); err != nil {
					b.Fatalf("failed to drop caches: %v", err)
				}

				// Run fio.
				b.StartTimer()
				data, err := container.Exec(ctx, dockerutil.ExecOpts{}, cmd...)
				if err != nil {
					b.Fatalf("failed to run cmd %v: %v", cmd, err)
				}
				b.StopTimer()
				tc.Report(b, data)
			})
		}
	}
}

// TestMain is the main method for package fs.
func TestMain(m *testing.M) {
	harness.Init()
	os.Exit(m.Run())
}