diff --git a/.github/workflows/xk6.yml b/.github/workflows/xk6.yml index c3829e8e0cf..2daf8c1bf40 100644 --- a/.github/workflows/xk6.yml +++ b/.github/workflows/xk6.yml @@ -14,6 +14,7 @@ defaults: jobs: test-xk6: strategy: + fail-fast: false matrix: go: [stable, tip] platform: [ubuntu-latest, windows-latest, macos-latest] diff --git a/README.md b/README.md index 2e0eeda1dda..8b15245b098 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ export const options = { // Simulated user behavior export default function () { - let res = http.get("https://test-api.k6.io/public/crocodiles/1/"); + let res = http.get("https://quickpizza.grafana.com"); // Validate response status check(res, { "status was 200": (r) => r.status == 200 }); sleep(1); diff --git a/examples/experimental/redis.js b/examples/experimental/redis.js index bf1bc9704a7..660ecff52db 100644 --- a/examples/experimental/redis.js +++ b/examples/experimental/redis.js @@ -1,87 +1,83 @@ -import { check } from "k6"; -import http from "k6/http"; -import redis from "k6/experimental/redis"; -import exec from "k6/execution"; -import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js"; +import { check } from 'k6'; +import http from 'k6/http'; +import redis from 'k6/experimental/redis'; +import exec from 'k6/execution'; +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; + export const options = { scenarios: { redisPerformance: { - executor: "shared-iterations", + executor: 'shared-iterations', vus: 10, iterations: 200, - exec: "measureRedisPerformance", + exec: 'measureRedisPerformance', }, usingRedisData: { - executor: "shared-iterations", + executor: 'shared-iterations', vus: 10, iterations: 200, - exec: "measureUsingRedisData", + exec: 'measureUsingRedisData', }, }, }; -// Get the redis instance(s) address and password from the environment -const redis_addrs = __ENV.REDIS_ADDRS || ""; -const redis_password = __ENV.REDIS_PASSWORD || ""; + // Instantiate a new redis client -const redisClient = new redis.Client({ - addrs: redis_addrs.split(",") || new Array("localhost:6379"), // in the form of 'host:port', separated by commas - password: redis_password, -}); -// Prepare an array of crocodile ids for later use +const redisClient = new redis.Client(`redis://localhost:6379`); + +// Prepare an array of rating ids for later use // in the context of the measureUsingRedisData function. -const crocodileIDs = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); -export function measureRedisPerformance() { +const ratingIDs = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + +export async function measureRedisPerformance() { // VUs are executed in a parallel fashion, // thus, to ensure that parallel VUs are not // modifying the same key at the same time, // we use keys indexed by the VU id. const key = `foo-${exec.vu.idInTest}`; - redisClient - .set(`foo-${exec.vu.idInTest}`, 1) - .then(() => redisClient.get(`foo-${exec.vu.idInTest}`)) - .then((value) => redisClient.incrBy(`foo-${exec.vu.idInTest}`, value)) - .then((_) => redisClient.del(`foo-${exec.vu.idInTest}`)) - .then((_) => redisClient.exists(`foo-${exec.vu.idInTest}`)) - .then((exists) => { - if (exists !== 0) { - throw new Error("foo should have been deleted"); - } - }); + + await redisClient.set(key, 1); + await redisClient.incrBy(key, 10); + const value = await redisClient.get(key); + if (value !== '11') { + throw new Error('foo should have been incremented to 11'); + } + + await redisClient.del(key); + if ((await redisClient.exists(key)) !== 0) { + throw new Error('foo should have been deleted'); + } } -export function setup() { - redisClient.sadd("crocodile_ids", ...crocodileIDs); + +export async function setup() { + await redisClient.sadd('rating_ids', ...ratingIDs); } -export function measureUsingRedisData() { - // Pick a random crocodile id from the dedicated redis set, + +export async function measureUsingRedisData() { + // Pick a random rating id from the dedicated redis set, // we have filled in setup(). - redisClient - .srandmember("crocodile_ids") - .then((randomID) => { - const url = `https://test-api.k6.io/public/crocodiles/${randomID}`; - const res = http.get(url); - check(res, { - "status is 200": (r) => r.status === 200, - "content-type is application/json": (r) => - r.headers["content-type"] === "application/json", - }); - return url; - }) - .then((url) => redisClient.hincrby("k6_crocodile_fetched", url, 1)); + const randomID = await redisClient.srandmember('rating_ids'); + const url = `https://quickpizza.grafana.com/api/ratings/${randomID}`; + const res = await http.asyncRequest('GET', url, { + headers: { Authorization: 'token abcdef0123456789' }, + }); + + check(res, { 'status is 200': (r) => r.status === 200 }); + + await redisClient.hincrby('k6_rating_fetched', url, 1); } -export function teardown() { - redisClient.del("crocodile_ids"); + +export async function teardown() { + await redisClient.del('rating_ids'); } + export function handleSummary(data) { redisClient - .hgetall("k6_crocodile_fetched") - .then((fetched) => - Object.assign(data, { k6_crocodile_fetched: fetched }) - ) - .then((data) => - redisClient.set(`k6_report_${Date.now()}`, JSON.stringify(data)) - ) - .then(() => redisClient.del("k6_crocodile_fetched")); + .hgetall('k6_rating_fetched') + .then((fetched) => Object.assign(data, { k6_rating_fetched: fetched })) + .then((data) => redisClient.set(`k6_report_${Date.now()}`, JSON.stringify(data))) + .then(() => redisClient.del('k6_rating_fetched')); + return { - stdout: textSummary(data, { indent: " ", enableColors: true }), + stdout: textSummary(data, { indent: ' ', enableColors: true }), }; } diff --git a/examples/experimental/websockets/test-api.k6.io.js b/examples/experimental/websockets/test-api.k6.io.js deleted file mode 100644 index 8d2ee5ae449..00000000000 --- a/examples/experimental/websockets/test-api.k6.io.js +++ /dev/null @@ -1,58 +0,0 @@ -import { randomString, randomIntBetween } from "https://jslib.k6.io/k6-utils/1.1.0/index.js"; -import { WebSocket } from "k6/experimental/websockets" - -let chatRoomName = 'publicRoom'; // choose your chat room name -let sessionDuration = randomIntBetween(5000, 60000); // user session between 5s and 1m - - -export default function() { - for (let i = 0; i < 4; i++) { - startWSWorker(i) - } -} - -function startWSWorker(id) { - let url = `wss://test-api.k6.io/ws/crocochat/${chatRoomName}/`; - let ws = new WebSocket(url); - ws.binaryType = "arraybuffer"; - ws.addEventListener("open", () => { - ws.send(JSON.stringify({ 'event': 'SET_NAME', 'new_name': `Croc ${__VU}:${id}` })); - - ws.addEventListener("message", (e) => { - let msg = JSON.parse(e.data); - if (msg.event === 'CHAT_MSG') { - console.log(`VU ${__VU}:${id} received: ${msg.user} says: ${msg.message}`) - } - else if (msg.event === 'ERROR') { - console.error(`VU ${__VU}:${id} received:: ${msg.message}`) - } - else { - console.log(`VU ${__VU}:${id} received unhandled message: ${msg.message}`) - } - }) - - - let intervalId = setInterval(() => { - ws.send(JSON.stringify({ 'event': 'SAY', 'message': `I'm saying ${randomString(5)}` })); - }, randomIntBetween(2000, 8000)); // say something every 2-8seconds - - - let timeout1id = setTimeout(function() { - clearInterval(intervalId) - console.log(`VU ${__VU}:${id}: ${sessionDuration}ms passed, leaving the chat`); - ws.send(JSON.stringify({ 'event': 'LEAVE' })); - }, sessionDuration); - - let timeout2id = setTimeout(function() { - console.log(`Closing the socket forcefully 3s after graceful LEAVE`); - ws.close(); - }, sessionDuration + 3000); - - ws.addEventListener("close", () => { - clearTimeout(timeout1id); - clearTimeout(timeout2id); - console.log(`VU ${__VU}:${id}: disconnected`); - }) - }); -} - diff --git a/examples/experimental/ws.js b/examples/experimental/ws.js index efc6b87be00..fdc1e596cd6 100644 --- a/examples/experimental/ws.js +++ b/examples/experimental/ws.js @@ -1,76 +1,55 @@ -import { - randomString, - randomIntBetween, -} from "https://jslib.k6.io/k6-utils/1.1.0/index.js"; -import { WebSocket } from "k6/experimental/websockets"; -import { - setTimeout, - clearTimeout, - setInterval, - clearInterval, -} from "k6/timers"; +import { randomString, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js'; +import { WebSocket } from 'k6/experimental/websockets'; -let chatRoomName = "publicRoom"; // choose your chat room name -let sessionDuration = randomIntBetween(5000, 60000); // user session between 5s and 1m +const sessionDuration = randomIntBetween(1000, 3000); // user session between 1s and 3s -export default function() { +export default function () { for (let i = 0; i < 4; i++) { startWSWorker(i); } } function startWSWorker(id) { - let url = `wss://test-api.k6.io/ws/crocochat/${chatRoomName}/`; - let ws = new WebSocket(url); - ws.binaryType = "arraybuffer"; - ws.addEventListener("open", () => { - ws.send( - JSON.stringify({ - event: "SET_NAME", - new_name: `Croc ${__VU}:${id}`, - }) - ); - - ws.addEventListener("message", (e) => { - let msg = JSON.parse(e.data); - if (msg.event === "CHAT_MSG") { - console.log( - `VU ${__VU}:${id} received: ${msg.user} says: ${msg.message}` - ); - } else if (msg.event === "ERROR") { + // create a new websocket connection + const ws = new WebSocket(`wss://quickpizza.grafana.com/ws`); + ws.binaryType = 'arraybuffer'; + + ws.addEventListener('open', () => { + // change the user name + ws.send(JSON.stringify({ event: 'SET_NAME', new_name: `VU ${__VU}:${id}` })); + + // listen for messages/errors and log them into console + ws.addEventListener('message', (e) => { + const msg = JSON.parse(e.data); + if (msg.event === 'CHAT_MSG') { + console.log(`VU ${__VU}:${id} received: ${msg.user} says: ${msg.message}`); + } else if (msg.event === 'ERROR') { console.error(`VU ${__VU}:${id} received:: ${msg.message}`); } else { - console.log( - `VU ${__VU}:${id} received unhandled message: ${msg.message}` - ); + console.log(`VU ${__VU}:${id} received unhandled message: ${msg.message}`); } }); - let intervalId = setInterval(() => { - ws.send( - JSON.stringify({ - event: "SAY", - message: `I'm saying ${randomString(5)}`, - }) - ); - }, randomIntBetween(2000, 8000)); // say something every 2-8seconds + // send a message every 2-8 seconds + const intervalId = setInterval(() => { + ws.send(JSON.stringify({ event: 'SAY', message: `I'm saying ${randomString(5)}` })); + }, randomIntBetween(2000, 8000)); // say something every 2-8 seconds - let timeout1id = setTimeout(function() { + // after a sessionDuration stop sending messages and leave the room + const timeout1id = setTimeout(function () { clearInterval(intervalId); - console.log( - `VU ${__VU}:${id}: ${sessionDuration}ms passed, leaving the chat` - ); - ws.send(JSON.stringify({ event: "LEAVE" })); + console.log(`VU ${__VU}:${id}: ${sessionDuration}ms passed, leaving the chat`); + ws.send(JSON.stringify({ event: 'LEAVE' })); }, sessionDuration); - let timeout2id = setTimeout(function() { - console.log( - `Closing the socket forcefully 3s after graceful LEAVE` - ); + // after a sessionDuration + 3s close the connection + const timeout2id = setTimeout(function () { + console.log(`Closing the socket forcefully 3s after graceful LEAVE`); ws.close(); }, sessionDuration + 3000); - ws.addEventListener("close", () => { + // when connection is closing, clean up the previously created timers + ws.addEventListener('close', () => { clearTimeout(timeout1id); clearTimeout(timeout2id); console.log(`VU ${__VU}:${id}: disconnected`); diff --git a/examples/http_2.js b/examples/http_2.js index a1d1a356c7f..3fa8ebbfeeb 100644 --- a/examples/http_2.js +++ b/examples/http_2.js @@ -2,7 +2,7 @@ import http from "k6/http"; import { check } from "k6"; export default function () { - check(http.get("https://test-api.k6.io/"), { + check(http.get("https://quickpizza.grafana.com"), { "status is 200": (r) => r.status == 200, "protocol is HTTP/2": (r) => r.proto == "HTTP/2.0", }); diff --git a/examples/http_get.js b/examples/http_get.js index 2eb256e407c..029fa72169a 100644 --- a/examples/http_get.js +++ b/examples/http_get.js @@ -1,5 +1,5 @@ import http from 'k6/http'; export default function () { - http.get('https://test-api.k6.io/'); + http.get('https://quickpizza.grafana.com'); }; diff --git a/internal/js/bundle_test.go b/internal/js/bundle_test.go index bc3f564b8bc..3a170f79c03 100644 --- a/internal/js/bundle_test.go +++ b/internal/js/bundle_test.go @@ -640,6 +640,11 @@ func TestOpen(t *testing.T) { openPath: "/path/to/file.txt", pwd: "/path", }, + { + name: "file scheme", + openPath: "file:///path/to/file.txt", + pwd: "/path", + }, { name: "file is dir", openPath: "/path/to/", @@ -687,6 +692,10 @@ func TestOpen(t *testing.T) { filePath := filepath.Join(prefix, "/path/to/file.txt") require.NoError(t, fs.MkdirAll(filepath.Join(prefix, "/path/to"), 0o755)) require.NoError(t, fsext.WriteFile(fs, filePath, []byte(`hi`), 0o644)) + fs = fsext.NewChangePathFs(fs, func(name string) (string, error) { + // Drop the prefix effectively building something like https://pkg.go.dev/os#DirFS + return filepath.Join(prefix, name), nil + }) if isWindows { fs = fsext.NewTrimFilePathSeparatorFs(fs) } @@ -705,14 +714,11 @@ func TestOpen(t *testing.T) { testFunc := func(t *testing.T) { t.Parallel() - fs, prefix, cleanUp := fsInit() + fs, _, cleanUp := fsInit() defer cleanUp() fs = fsext.NewReadOnlyFs(fs) openPath := tCase.openPath // if fullpath prepend prefix - if openPath != "" && (openPath[0] == '/' || openPath[0] == '\\') { - openPath = filepath.Join(prefix, openPath) - } if isWindows { openPath = strings.ReplaceAll(openPath, `\`, `\\`) } @@ -724,7 +730,7 @@ func TestOpen(t *testing.T) { export let file = open("` + openPath + `"); export default function() { return file };` - sourceBundle, err := getSimpleBundle(t, filepath.ToSlash(filepath.Join(prefix, pwd, "script.js")), data, fs) + sourceBundle, err := getSimpleBundle(t, filepath.ToSlash(filepath.Join(pwd, "script.js")), data, fs) if tCase.isError { require.Error(t, err) return @@ -749,6 +755,7 @@ func TestOpen(t *testing.T) { t.Run(tCase.name, testFunc) if isWindows { + tCase := tCase // copy test case before making modifications // windowsify the testcase tCase.openPath = strings.ReplaceAll(tCase.openPath, `/`, `\`) tCase.pwd = strings.ReplaceAll(tCase.pwd, `/`, `\`) diff --git a/internal/js/initcontext.go b/internal/js/initcontext.go index b4997b3d972..8fcb9f62648 100644 --- a/internal/js/initcontext.go +++ b/internal/js/initcontext.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/url" + "strings" "github.com/grafana/sobek" "github.com/sirupsen/logrus" @@ -19,6 +20,8 @@ const cantBeUsedOutsideInitContextMsg = `the "%s" function is only available in // contents of a file. If the second argument is "b" it returns an ArrayBuffer // instance, otherwise a string representation. func openImpl(rt *sobek.Runtime, fs fsext.Fs, basePWD *url.URL, filename string, args ...string) (sobek.Value, error) { + // Strip file scheme if available as we should support only this scheme + filename = strings.TrimPrefix(filename, "file://") data, err := readFile(fs, fsext.Abs(basePWD.Path, filename)) if err != nil { return nil, err diff --git a/internal/js/modules/k6/experimental/fs/module.go b/internal/js/modules/k6/experimental/fs/module.go index 9045d8210c1..34aee8adc62 100644 --- a/internal/js/modules/k6/experimental/fs/module.go +++ b/internal/js/modules/k6/experimental/fs/module.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "reflect" + "strings" "go.k6.io/k6/lib/fsext" @@ -102,6 +103,8 @@ func (mi *ModuleInstance) Open(path sobek.Value) *sobek.Promise { func (mi *ModuleInstance) openImpl(path string) (*File, error) { initEnv := mi.vu.InitEnv() + // Strip file scheme if available as we should support only this scheme + path = strings.TrimPrefix(path, "file://") // We resolve the path relative to the entrypoint script, as opposed to // the current working directory (the k6 command is called from). diff --git a/internal/js/modules/k6/experimental/fs/module_test.go b/internal/js/modules/k6/experimental/fs/module_test.go index fddd59c649f..4302b8e6953 100644 --- a/internal/js/modules/k6/experimental/fs/module_test.go +++ b/internal/js/modules/k6/experimental/fs/module_test.go @@ -33,6 +33,11 @@ func TestOpen(t *testing.T) { openPath: fsext.FilePathSeparator + testFileName, wantPath: fsext.FilePathSeparator + testFileName, }, + { + name: "open file absolute path", + openPath: "file://" + fsext.FilePathSeparator + testFileName, + wantPath: fsext.FilePathSeparator + testFileName, + }, { name: "open relative path", openPath: filepath.Join(".", fsext.FilePathSeparator, testFileName), diff --git a/internal/js/modules/k6/grpc/client.go b/internal/js/modules/k6/grpc/client.go index 20b507829ff..b04c7d36ebc 100644 --- a/internal/js/modules/k6/grpc/client.go +++ b/internal/js/modules/k6/grpc/client.go @@ -54,6 +54,11 @@ func (c *Client) Load(importPaths []string, filenames ...string) ([]MethodInfo, importPaths = append(importPaths, initEnv.CWD.Path) } + for i, s := range importPaths { + // Clean file scheme as it is the only supported scheme and the following APIs do not support them + importPaths[i] = strings.TrimPrefix(s, "file://") + } + parser := protoparse.Parser{ ImportPaths: importPaths, InferImportPaths: false, diff --git a/internal/js/modules/k6/grpc/client_test.go b/internal/js/modules/k6/grpc/client_test.go index e9dd8c864dc..358bfc1cfe0 100644 --- a/internal/js/modules/k6/grpc/client_test.go +++ b/internal/js/modules/k6/grpc/client_test.go @@ -1374,7 +1374,6 @@ func TestClientLoadProto(t *testing.T) { ts := newTestState(t) tt := testcase{ - name: "LoadNestedTypesProto", initString: codeBlock{ code: ` var client = new grpc.Client(); @@ -1402,6 +1401,40 @@ func TestClientLoadProto(t *testing.T) { } } +func TestClientLoadProtoAbsoluteRootWithFile(t *testing.T) { + t.Parallel() + + ts := newTestState(t) + rootPath := ts.VU.InitEnvField.CWD.JoinPath("../..").String() + + tt := testcase{ + initString: codeBlock{ + code: ` + var client = new grpc.Client(); + client.load(["` + rootPath + `"], "../../lib/testutils/httpmultibin/nested_types/nested_types.proto");`, + }, + } + + val, err := ts.Run(tt.initString.code) + assertResponse(t, tt.initString, err, val, ts) + + expectedTypes := []string{ + "grpc.testdata.nested.types.Outer", + "grpc.testdata.nested.types.Outer.MiddleAA", + "grpc.testdata.nested.types.Outer.MiddleAA.Inner", + "grpc.testdata.nested.types.Outer.MiddleBB", + "grpc.testdata.nested.types.Outer.MiddleBB.Inner", + "grpc.testdata.nested.types.MeldOuter", + } + + for _, expected := range expectedTypes { + found, err := protoregistry.GlobalTypes.FindMessageByName(protoreflect.FullName(expected)) + + assert.NotNil(t, found, "Expected to find the message type %s, but an error occurred", expected) + assert.Nil(t, err, "It was not expected that there would be an error, but it got: %v", err) + } +} + func TestClientConnectionReflectMetadata(t *testing.T) { t.Parallel() diff --git a/internal/js/modules/k6/grpc/teststate_test.go b/internal/js/modules/k6/grpc/teststate_test.go index 830eef515a4..d8137378607 100644 --- a/internal/js/modules/k6/grpc/teststate_test.go +++ b/internal/js/modules/k6/grpc/teststate_test.go @@ -4,6 +4,7 @@ import ( "io" "net/url" "os" + "path/filepath" "runtime" "sync" "testing" @@ -108,7 +109,7 @@ func newTestState(t *testing.T) testState { if isWindows { fs = fsext.NewTrimFilePathSeparatorFs(fs) } - testRuntime.VU.InitEnvField.CWD = &url.URL{Path: cwd} + testRuntime.VU.InitEnvField.CWD = &url.URL{Scheme: "file", Path: filepath.ToSlash(cwd)} testRuntime.VU.InitEnvField.FileSystems = map[string]fsext.Fs{"file": fs} logger := logrus.New()