Skip to content

Commit 3112abe

Browse files
authored
fix(runner): limit concurrency per task branch in addition to per leaf callbacks (#10179)
1 parent 32bb903 commit 3112abe

2 files changed

Lines changed: 295 additions & 1 deletion

File tree

packages/runner/src/run.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,8 @@ export async function runSuite(suite: Suite, runner: VitestRunner): Promise<void
898898
else {
899899
for (let tasksGroup of partitionSuiteChildren(suite)) {
900900
if (tasksGroup[0].concurrent === true) {
901-
await Promise.all(tasksGroup.map(c => runSuiteChild(c, runner)))
901+
const groupLimiter = limitConcurrency(runner.config.maxConcurrency)
902+
await Promise.all(tasksGroup.map(c => groupLimiter(() => runSuiteChild(c, runner))))
902903
}
903904
else {
904905
const { sequence } = runner.config

test/e2e/test/concurrent.test.ts

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,3 +1074,296 @@ test('aroundAll enforces teardown timeout when inner error is caught', async ()
10741074
}
10751075
`)
10761076
})
1077+
1078+
function extractLogs(log: string) {
1079+
const result = log.split('\n').filter(line => line.match(/^![<>]/)).join('\n')
1080+
return `\n${result.trim()}\n`
1081+
}
1082+
1083+
test('sibling task sequential lifecycle guarantee', async () => {
1084+
const result = await runInlineTests({
1085+
'basic.test.ts': `
1086+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
1087+
1088+
beforeEach(async ({ task }) => {
1089+
console.log("!> beforeEach", task.name)
1090+
await sleep(10)
1091+
console.log("!< beforeEach", task.name)
1092+
})
1093+
1094+
afterEach(async ({ task }) => {
1095+
console.log("!> afterEach", task.name)
1096+
await sleep(10)
1097+
console.log("!< afterEach", task.name)
1098+
})
1099+
1100+
test.concurrent.for(["a", "b"])("%s", async (_, { task }) => {
1101+
console.log("!> test", task.name)
1102+
await sleep(10)
1103+
console.log("!< test", task.name)
1104+
})
1105+
`,
1106+
}, {
1107+
maxConcurrency: 1,
1108+
globals: true,
1109+
})
1110+
1111+
expect(extractLogs(result.stdout)).toMatchInlineSnapshot(`
1112+
"
1113+
!> beforeEach a
1114+
!< beforeEach a
1115+
!> test a
1116+
!< test a
1117+
!> afterEach a
1118+
!< afterEach a
1119+
!> beforeEach b
1120+
!< beforeEach b
1121+
!> test b
1122+
!< test b
1123+
!> afterEach b
1124+
!< afterEach b
1125+
"
1126+
`)
1127+
expect(result.errorTree()).toMatchInlineSnapshot(`
1128+
{
1129+
"basic.test.ts": {
1130+
"a": "passed",
1131+
"b": "passed",
1132+
},
1133+
}
1134+
`)
1135+
})
1136+
1137+
test('sibling suite sequential lifecycle guarantee', async () => {
1138+
const result = await runInlineTests({
1139+
'basic.test.ts': `
1140+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
1141+
1142+
describe.for(["a", "b"])("%s", { concurrent: true }, () => {
1143+
beforeAll(async ({}, suite) => {
1144+
console.log("!> beforeAll", suite.name)
1145+
await sleep(10)
1146+
console.log("!< beforeAll", suite.name)
1147+
})
1148+
1149+
afterAll(async ({}, suite) => {
1150+
console.log("!> afterAll", suite.name)
1151+
await sleep(10)
1152+
console.log("!< afterAll", suite.name)
1153+
})
1154+
1155+
test.concurrent("test", async ({ task }) => {
1156+
console.log("!> test", task.suite.name)
1157+
await sleep(10)
1158+
console.log("!< test", task.suite.name)
1159+
})
1160+
})
1161+
`,
1162+
}, {
1163+
maxConcurrency: 1,
1164+
globals: true,
1165+
})
1166+
1167+
expect(extractLogs(result.stdout)).toMatchInlineSnapshot(`
1168+
"
1169+
!> beforeAll a
1170+
!< beforeAll a
1171+
!> test a
1172+
!< test a
1173+
!> afterAll a
1174+
!< afterAll a
1175+
!> beforeAll b
1176+
!< beforeAll b
1177+
!> test b
1178+
!< test b
1179+
!> afterAll b
1180+
!< afterAll b
1181+
"
1182+
`)
1183+
expect(result.errorTree()).toMatchInlineSnapshot(`
1184+
{
1185+
"basic.test.ts": {
1186+
"a": {
1187+
"test": "passed",
1188+
},
1189+
"b": {
1190+
"test": "passed",
1191+
},
1192+
},
1193+
}
1194+
`)
1195+
})
1196+
1197+
// we could enforce this by adding yet another limit globally at `runTest`
1198+
// (like we originally had before https://github.com/vitest-dev/vitest/pull/9653)
1199+
// but there's no way to achieve the same for deep suite-level hooks anyways,
1200+
// so we don't do that (yet).
1201+
test('non-sibling test sequential lifecycle non-guarantee', async () => {
1202+
const result = await runInlineTests({
1203+
'basic.test.ts': `
1204+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
1205+
1206+
describe.for(["a0", "a1"])("%s", { concurrent: true }, () => {
1207+
describe.for(["b0", "b1"])("%s", { concurrent: true }, () => {
1208+
beforeEach(async ({ task }) => {
1209+
console.log("!> beforeEach", task.suite.suite.name, task.suite.name, task.name)
1210+
await sleep(10)
1211+
console.log("!< beforeEach", task.suite.suite.name, task.suite.name, task.name)
1212+
})
1213+
1214+
afterEach(async ({ task }) => {
1215+
console.log("!> afterEach", task.suite.suite.name, task.suite.name, task.name)
1216+
await sleep(10)
1217+
console.log("!< afterEach", task.suite.suite.name, task.suite.name, task.name)
1218+
})
1219+
1220+
test("test", async ({ task }) => {
1221+
console.log("!> test", task.suite.suite.name,task.suite.name, task.name)
1222+
await sleep(10)
1223+
console.log("!< test", task.suite.suite.name,task.suite.name, task.name)
1224+
})
1225+
})
1226+
})
1227+
`,
1228+
}, {
1229+
maxConcurrency: 2,
1230+
globals: true,
1231+
})
1232+
1233+
expect(extractLogs(result.stdout)).toMatchInlineSnapshot(`
1234+
"
1235+
!> beforeEach a0 b0 test
1236+
!> beforeEach a0 b1 test
1237+
!< beforeEach a0 b0 test
1238+
!> beforeEach a1 b0 test
1239+
!< beforeEach a0 b1 test
1240+
!> beforeEach a1 b1 test
1241+
!< beforeEach a1 b0 test
1242+
!> test a0 b0 test
1243+
!< beforeEach a1 b1 test
1244+
!> test a0 b1 test
1245+
!< test a0 b0 test
1246+
!> test a1 b0 test
1247+
!< test a0 b1 test
1248+
!> test a1 b1 test
1249+
!< test a1 b0 test
1250+
!> afterEach a0 b0 test
1251+
!< test a1 b1 test
1252+
!> afterEach a0 b1 test
1253+
!< afterEach a0 b0 test
1254+
!> afterEach a1 b0 test
1255+
!< afterEach a0 b1 test
1256+
!> afterEach a1 b1 test
1257+
!< afterEach a1 b0 test
1258+
!< afterEach a1 b1 test
1259+
"
1260+
`)
1261+
1262+
expect(result.errorTree()).toMatchInlineSnapshot(`
1263+
{
1264+
"basic.test.ts": {
1265+
"a0": {
1266+
"b0": {
1267+
"test": "passed",
1268+
},
1269+
"b1": {
1270+
"test": "passed",
1271+
},
1272+
},
1273+
"a1": {
1274+
"b0": {
1275+
"test": "passed",
1276+
},
1277+
"b1": {
1278+
"test": "passed",
1279+
},
1280+
},
1281+
},
1282+
}
1283+
`)
1284+
})
1285+
1286+
test('non-sibling suite sequential lifecycle non-guarantee', async () => {
1287+
const result = await runInlineTests({
1288+
'basic.test.ts': `
1289+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
1290+
1291+
describe.for(["a0", "a1"])("%s", { concurrent: true }, () => {
1292+
describe.for(["b0", "b1"])("%s", { concurrent: true }, () => {
1293+
beforeAll(async ({}, suite) => {
1294+
console.log("!> beforeAll", suite.suite.name, suite.name)
1295+
await sleep(10)
1296+
console.log("!< beforeAll", suite.suite.name, suite.name)
1297+
})
1298+
1299+
afterAll(async ({}, suite) => {
1300+
console.log("!> afterAll", suite.suite.name, suite.name)
1301+
await sleep(10)
1302+
console.log("!< afterAll", suite.suite.name, suite.name)
1303+
})
1304+
1305+
test("test", async ({ task }) => {
1306+
console.log("!> test", task.suite.suite.name, task.suite.name, task.name)
1307+
await sleep(10)
1308+
console.log("!< test", task.suite.suite.name, task.suite.name, task.name)
1309+
})
1310+
})
1311+
})
1312+
`,
1313+
}, {
1314+
maxConcurrency: 2,
1315+
globals: true,
1316+
})
1317+
1318+
expect(extractLogs(result.stdout)).toMatchInlineSnapshot(`
1319+
"
1320+
!> beforeAll a0 b0
1321+
!> beforeAll a0 b1
1322+
!< beforeAll a0 b0
1323+
!> beforeAll a1 b0
1324+
!< beforeAll a0 b1
1325+
!> beforeAll a1 b1
1326+
!< beforeAll a1 b0
1327+
!> test a0 b0 test
1328+
!< beforeAll a1 b1
1329+
!> test a0 b1 test
1330+
!< test a0 b0 test
1331+
!> test a1 b0 test
1332+
!< test a0 b1 test
1333+
!> test a1 b1 test
1334+
!< test a1 b0 test
1335+
!> afterAll a0 b0
1336+
!< test a1 b1 test
1337+
!> afterAll a0 b1
1338+
!< afterAll a0 b0
1339+
!> afterAll a1 b0
1340+
!< afterAll a0 b1
1341+
!> afterAll a1 b1
1342+
!< afterAll a1 b0
1343+
!< afterAll a1 b1
1344+
"
1345+
`)
1346+
1347+
expect(result.errorTree()).toMatchInlineSnapshot(`
1348+
{
1349+
"basic.test.ts": {
1350+
"a0": {
1351+
"b0": {
1352+
"test": "passed",
1353+
},
1354+
"b1": {
1355+
"test": "passed",
1356+
},
1357+
},
1358+
"a1": {
1359+
"b0": {
1360+
"test": "passed",
1361+
},
1362+
"b1": {
1363+
"test": "passed",
1364+
},
1365+
},
1366+
},
1367+
}
1368+
`)
1369+
})

0 commit comments

Comments
 (0)