Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -499,9 +499,9 @@ private List<RequestInfo> parseRequestsWithTiming(List<String> lines) {
}

if (inRequest) {
int opens = countOccurrences(line, '(');
int opens = countUnquotedOccurrences(line, '(');
parenCount += opens;
parenCount -= countOccurrences(line, ')');
parenCount -= countUnquotedOccurrences(line, ')');
if (opens > 0) {
seenOpenParen = true;
}
Expand Down Expand Up @@ -647,8 +647,8 @@ private String insertJsessionExtraction(String content) {
}

if (inFirstAppRequest) {
braceCount += countOccurrences(line, '{');
braceCount -= countOccurrences(line, '}');
braceCount += countUnquotedOccurrences(line, '{');
braceCount -= countUnquotedOccurrences(line, '}');

if (braceCount == 0 && line.contains(")")) {
insertLineIndex = i;
Expand Down Expand Up @@ -687,8 +687,8 @@ private String insertVaadinExtraction(String content) {
}

if (inInitRequest) {
braceCount += countOccurrences(line, '{');
braceCount -= countOccurrences(line, '}');
braceCount += countUnquotedOccurrences(line, '{');
braceCount -= countUnquotedOccurrences(line, '}');

if (braceCount == 0 && line.contains(")")) {
insertLineIndex = i;
Expand All @@ -705,10 +705,35 @@ private String insertVaadinExtraction(String content) {
return content;
}

private int countOccurrences(String str, char c) {
/**
* Counts occurrences of {@code target} in {@code str}, ignoring any
* occurrences that fall inside a JavaScript string literal delimited by
* {@code '}, {@code "}, or {@code `}. Backslash escapes inside strings are
* honored. This is what keeps a {@code )} inside a header value like
* {@code 'sec-ch-ua': '"Not/A)Brand";v="99"'} from being mistaken for the
* closing paren of an {@code http.get(...)} call.
* <p>
* The scan is per-line: {@link HarToK6Converter} escapes
* {@code \n}/{@code \r}/ {@code \t} in URLs, headers, and bodies, so
* generated string literals never span multiple lines. Template-literal
* interpolations ({@code ${...}}) are treated as opaque string content —
* the converter only emits interpolations of bare identifiers, none of
* which contain {@code (}, {@code )}, {@code {}, or {@code }}.
*/
private int countUnquotedOccurrences(String str, char target) {
int count = 0;
char stringDelim = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == c) {
char c = str.charAt(i);
if (stringDelim != 0) {
if (c == '\\' && i + 1 < str.length()) {
i++;
} else if (c == stringDelim) {
stringDelim = 0;
}
} else if (c == '\'' || c == '"' || c == '`') {
stringDelim = c;
} else if (c == target) {
count++;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,64 @@ export default function() {
+ refactored);
}

@Test
void refactorContent_thinkTimesEnabled_headerValueContainsCloseParen_sleepIsNotInjectedInsideHeaders() {
K6TestRefactorer refactorer = new K6TestRefactorer(
ThinkTimeConfig.DEFAULT);

// Reproduces the combineScenarios=true breakage: the Chrome sec-ch-ua
// header value contains a literal `)` inside a single-quoted JS
// string. A non-string-aware paren counter dropped to zero mid-headers
// and the sleep() got injected inside the headers object, producing
// "Unexpected number" parse errors in k6.
String script = """
import http from 'k6/http'
import { sleep } from 'k6'
export default function() {
// HAR_DELTA_MS: 0
// Request 1: GET http://localhost:8080/?v-r=init
let response = http.get(
'http://localhost:8080/?v-r=init',
{
headers: {
'sec-ch-ua': '"Chromium";v="148", "Not/A)Brand";v="99"',
'User-Agent': 'Mozilla/5.0'
},
tags: { name: 'init' }
}
)
// HAR_DELTA_MS: 200
// Request 2: POST http://localhost:8080/?v-r=uidl
response = http.post('http://localhost:8080/?v-r=uidl', '{"event":"click"}')
}
""";

String refactored = refactorer.refactorContent(script);

// A sleep() call should be emitted (page-read delay at the init
// block boundary).
assertTrue(refactored.contains("sleep("),
"Expected at least one sleep() call in refactored output:\n"
+ refactored);

// The `Not/A)Brand` sec-ch-ua line is the bug trigger. After
// refactoring it must still be immediately followed by the next
// header property — not by a // Think time comment or a sleep()
// call, which would mean the sleep was injected inside the
// headers object.
String triggerLine = "'sec-ch-ua': '\"Chromium\";v=\"148\", \"Not/A)Brand\";v=\"99\"',";
int triggerIdx = refactored.indexOf(triggerLine);
assertTrue(triggerIdx >= 0,
"Expected sec-ch-ua header line to survive refactoring intact:\n"
+ refactored);
String afterTrigger = refactored
.substring(triggerIdx + triggerLine.length()).stripLeading();
assertTrue(afterTrigger.startsWith("'User-Agent'"),
"sec-ch-ua header was not followed by the User-Agent header — "
+ "a sleep() was likely injected inside the headers object:\n"
+ refactored);
}

@Test
void refactor_readsInputAndWritesOutputFile() throws IOException {
Path input = tempDir.resolve("in.js");
Expand Down
Loading