[{"data":1,"prerenderedAt":1997},["ShallowReactive",2],{"navigation":3,"\u002Fdeployment\u002Ffaster-functional-tests":413,"\u002Fdeployment\u002Ffaster-functional-tests-surround":1992},[4,14,36,69,144,384],{"title":5,"path":6,"stem":7,"children":8},"Introduction","\u002Fgetting-started","1.getting-started\u002F1.index",[9,10],{"title":5,"path":6,"stem":7},{"title":11,"path":12,"stem":13},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation",{"title":15,"path":16,"stem":17,"children":18,"page":35},"Guides","\u002Fguides","2.guides",[19,23,27,31],{"title":20,"path":21,"stem":22},"Your First Layout","\u002Fguides\u002Fyour-first-layout","2.guides\u002F1.your-first-layout",{"title":24,"path":25,"stem":26},"Your First Page Template","\u002Fguides\u002Fyour-first-page-template","2.guides\u002F2.your-first-page-template",{"title":28,"path":29,"stem":30},"Your First Component","\u002Fguides\u002Fyour-first-component","2.guides\u002F3.your-first-component",{"title":32,"path":33,"stem":34},"Alternative UI Variants","\u002Fguides\u002Falternative-ui-variants","2.guides\u002F4.alternative-ui-variants",false,{"title":37,"path":38,"stem":39,"children":40,"page":35},"Core Concepts","\u002Fcore-concepts","3.core-concepts",[41,45,49,53,57,61,65],{"title":42,"path":43,"stem":44},"How It All Works","\u002Fcore-concepts\u002Farchitecture","3.core-concepts\u002F1.architecture",{"title":46,"path":47,"stem":48},"The Data Model","\u002Fcore-concepts\u002Fthe-data-model","3.core-concepts\u002F2.the-data-model",{"title":50,"path":51,"stem":52},"Layouts & Pages","\u002Fcore-concepts\u002Flayouts-and-pages","3.core-concepts\u002F3.layouts-and-pages",{"title":54,"path":55,"stem":56},"Dynamic Pages","\u002Fcore-concepts\u002Fdynamic-pages","3.core-concepts\u002F4.dynamic-pages",{"title":58,"path":59,"stem":60},"Components","\u002Fcore-concepts\u002Fcomponents","3.core-concepts\u002F5.components",{"title":62,"path":63,"stem":64},"Draft & Publish Workflow","\u002Fcore-concepts\u002Fdraft-and-publish","3.core-concepts\u002F6.draft-and-publish",{"title":66,"path":67,"stem":68},"The Admin Panel","\u002Fcore-concepts\u002Fadmin-panel","3.core-concepts\u002F7.admin-panel",{"title":70,"path":71,"stem":72,"children":73,"page":35},"Api","\u002Fapi","4.api",[74,78,120,124,128,132,136,140],{"title":75,"path":76,"stem":77},"Bundle Setup","\u002Fapi\u002Fbundle-setup","4.api\u002F1.bundle-setup",{"title":58,"path":79,"stem":80,"children":81,"page":35},"\u002Fapi\u002Fcomponents","4.api\u002F2.components",[82,86,107],{"title":83,"path":84,"stem":85},"Creating Components","\u002Fapi\u002Fcomponents\u002Fcreating-components","4.api\u002F2.components\u002F1.creating-components",{"title":87,"path":88,"stem":89,"children":90,"page":35},"Annotations","\u002Fapi\u002Fcomponents\u002Fannotations","4.api\u002F2.components\u002F2.annotations",[91,95,99,103],{"title":92,"path":93,"stem":94},"Publishable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fpublishable","4.api\u002F2.components\u002F2.annotations\u002F1.publishable",{"title":96,"path":97,"stem":98},"Uploadable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fuploadable","4.api\u002F2.components\u002F2.annotations\u002F2.uploadable",{"title":100,"path":101,"stem":102},"Timestamped","\u002Fapi\u002Fcomponents\u002Fannotations\u002Ftimestamped","4.api\u002F2.components\u002F2.annotations\u002F3.timestamped",{"title":104,"path":105,"stem":106},"Explicit Allow Only","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fexplicit-allow-only","4.api\u002F2.components\u002F2.annotations\u002F4.explicit-allow-only",{"title":108,"path":109,"stem":110,"children":111,"page":35},"Built Ins","\u002Fapi\u002Fcomponents\u002Fbuilt-ins","4.api\u002F2.components\u002F3.built-ins",[112,116],{"title":113,"path":114,"stem":115},"Collection Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fcollection-component","4.api\u002F2.components\u002F3.built-ins\u002F1.collection-component",{"title":117,"path":118,"stem":119},"Form Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fform-component","4.api\u002F2.components\u002F3.built-ins\u002F2.form-component",{"title":121,"path":122,"stem":123},"Dynamic & Nested Pages","\u002Fapi\u002Fdynamic-pages","4.api\u002F3.dynamic-pages",{"title":125,"path":126,"stem":127},"Users & Security","\u002Fapi\u002Fusers-and-security","4.api\u002F4.users-and-security",{"title":129,"path":130,"stem":131},"Data Fixtures","\u002Fapi\u002Fdata-fixtures","4.api\u002F5.data-fixtures",{"title":133,"path":134,"stem":135},"Configuration Reference","\u002Fapi\u002Fconfiguration","4.api\u002F6.configuration",{"title":137,"path":138,"stem":139},"Console Commands","\u002Fapi\u002Fconsole-commands","4.api\u002F7.console-commands",{"title":141,"path":142,"stem":143},"Debugging & Profiler","\u002Fapi\u002Fdebugging","4.api\u002F8.debugging",{"title":145,"path":146,"stem":147,"children":148,"page":35},"Nuxt Module","\u002Fnuxt-module","5.nuxt-module",[149,153,157,161,174,194,215,248,332,356,360],{"title":150,"path":151,"stem":152},"Module Setup","\u002Fnuxt-module\u002Fmodule-setup","5.nuxt-module\u002F1.module-setup",{"title":154,"path":155,"stem":156},"PWA & Offline","\u002Fnuxt-module\u002Fpwa","5.nuxt-module\u002F10.pwa",{"title":158,"path":159,"stem":160},"Page Caching","\u002Fnuxt-module\u002Fpage-caching","5.nuxt-module\u002F11.page-caching",{"title":162,"path":163,"stem":164,"children":165,"page":35},"Configuration","\u002Fnuxt-module\u002Fconfiguration","5.nuxt-module\u002F2.configuration",[166,170],{"title":167,"path":168,"stem":169},"Nuxt Config","\u002Fnuxt-module\u002Fconfiguration\u002Fnuxt-config","5.nuxt-module\u002F2.configuration\u002F1.nuxt-config",{"title":171,"path":172,"stem":173},"Site Config & SEO","\u002Fnuxt-module\u002Fconfiguration\u002Fsite-config-and-seo","5.nuxt-module\u002F2.configuration\u002F2.site-config-and-seo",{"title":175,"path":176,"stem":177,"children":178,"page":35},"Building Your Ui","\u002Fnuxt-module\u002Fbuilding-your-ui","5.nuxt-module\u002F3.building-your-ui",[179,183,187,190],{"title":180,"path":181,"stem":182},"Layouts","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-layouts","5.nuxt-module\u002F3.building-your-ui\u002F1.creating-layouts",{"title":184,"path":185,"stem":186},"Page Templates","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-page-templates","5.nuxt-module\u002F3.building-your-ui\u002F2.creating-page-templates",{"title":83,"path":188,"stem":189},"\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-components","5.nuxt-module\u002F3.building-your-ui\u002F3.creating-components",{"title":191,"path":192,"stem":193},"CLI Generator","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcwa-cli","5.nuxt-module\u002F3.building-your-ui\u002F4.cwa-cli",{"title":195,"path":196,"stem":197,"children":198,"page":35},"Cwa Components","\u002Fnuxt-module\u002Fcwa-components","5.nuxt-module\u002F4.cwa-components",[199,203,207,211],{"title":200,"path":201,"stem":202},"\u003CCwaComponentGroup \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-component-group","5.nuxt-module\u002F4.cwa-components\u002F1.cwa-component-group",{"title":204,"path":205,"stem":206},"\u003CCwaPage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-page","5.nuxt-module\u002F4.cwa-components\u002F2.cwa-page",{"title":208,"path":209,"stem":210},"\u003CCwaLink \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-link","5.nuxt-module\u002F4.cwa-components\u002F3.cwa-link",{"title":212,"path":213,"stem":214},"\u003CCwaDefaultLayout \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-default-layout","5.nuxt-module\u002F4.cwa-components\u002F5.cwa-default-layout",{"title":216,"path":217,"stem":218,"children":219,"page":35},"Cwa Api","\u002Fnuxt-module\u002Fcwa-api","5.nuxt-module\u002F5.cwa-api",[220,224,228,232,236,240,244],{"title":221,"path":222,"stem":223},"Overview","\u002Fnuxt-module\u002Fcwa-api\u002Foverview","5.nuxt-module\u002F5.cwa-api\u002F1.overview",{"title":225,"path":226,"stem":227},"Resources","\u002Fnuxt-module\u002Fcwa-api\u002Fresources","5.nuxt-module\u002F5.cwa-api\u002F2.resources",{"title":229,"path":230,"stem":231},"Resources Manager","\u002Fnuxt-module\u002Fcwa-api\u002Fresources-manager","5.nuxt-module\u002F5.cwa-api\u002F3.resources-manager",{"title":233,"path":234,"stem":235},"Auth","\u002Fnuxt-module\u002Fcwa-api\u002Fauth","5.nuxt-module\u002F5.cwa-api\u002F4.auth",{"title":237,"path":238,"stem":239},"Forms","\u002Fnuxt-module\u002Fcwa-api\u002Fforms","5.nuxt-module\u002F5.cwa-api\u002F5.forms",{"title":241,"path":242,"stem":243},"Admin","\u002Fnuxt-module\u002Fcwa-api\u002Fadmin","5.nuxt-module\u002F5.cwa-api\u002F6.admin",{"title":245,"path":246,"stem":247},"Site Config","\u002Fnuxt-module\u002Fcwa-api\u002Fsite-config","5.nuxt-module\u002F5.cwa-api\u002F7.site-config",{"title":249,"path":250,"stem":251,"children":252,"page":35},"Composables","\u002Fnuxt-module\u002Fcomposables","5.nuxt-module\u002F6.composables",[253,261,298,315],{"title":254,"path":255,"stem":256,"children":257,"page":35},"Layout","\u002Fnuxt-module\u002Fcomposables\u002Flayout","5.nuxt-module\u002F6.composables\u002F0.layout",[258],{"title":254,"path":259,"stem":260},"\u002Fnuxt-module\u002Fcomposables\u002Flayout\u002Fuse-cwa-layout","5.nuxt-module\u002F6.composables\u002F0.layout\u002F1.use-cwa-layout",{"title":262,"path":263,"stem":264,"children":265,"page":35},"Component","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent","5.nuxt-module\u002F6.composables\u002F1.component",[266,270,274,278,282,286,290,294],{"title":267,"path":268,"stem":269},"Component (recommended)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-component","5.nuxt-module\u002F6.composables\u002F1.component\u002F0.use-cwa-component",{"title":271,"path":272,"stem":273},"Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F1.use-cwa-resource",{"title":275,"path":276,"stem":277},"Collection Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F2.use-cwa-collection-resource",{"title":279,"path":280,"stem":281},"File Field","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-file-field","5.nuxt-module\u002F6.composables\u002F1.component\u002F3.use-cwa-file-field",{"title":283,"path":284,"stem":285},"Form","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form","5.nuxt-module\u002F6.composables\u002F1.component\u002F4.use-cwa-form",{"title":287,"path":288,"stem":289},"Form Input","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-input","5.nuxt-module\u002F6.composables\u002F1.component\u002F5.use-cwa-form-input",{"title":291,"path":292,"stem":293},"Form Repeated","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-repeated","5.nuxt-module\u002F6.composables\u002F1.component\u002F6.use-cwa-form-repeated",{"title":295,"path":296,"stem":297},"Form Collection","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-collection","5.nuxt-module\u002F6.composables\u002F1.component\u002F7.use-cwa-form-collection",{"title":299,"path":300,"stem":301,"children":302,"page":35},"Admin Manager","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager","5.nuxt-module\u002F6.composables\u002F2.admin-manager",[303,307,311],{"title":304,"path":305,"stem":306},"Manager Tab","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-manager-tab","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F1.use-cwa-resource-manager-tab",{"title":308,"path":309,"stem":310},"Resource Model","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-model","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F2.use-cwa-resource-model",{"title":312,"path":313,"stem":314},"Resource Upload","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-upload","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F3.use-cwa-resource-upload",{"title":316,"path":317,"stem":318,"children":319,"page":35},"Utilities","\u002Fnuxt-module\u002Fcomposables\u002Futilities","5.nuxt-module\u002F6.composables\u002F3.utilities",[320,324,328],{"title":321,"path":322,"stem":323},"Resource Endpoint","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-endpoint","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F1.use-cwa-resource-endpoint",{"title":325,"path":326,"stem":327},"Query Model","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-query-bound-model","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F2.use-query-bound-model",{"title":329,"path":330,"stem":331},"Resource Route","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-route","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F3.use-cwa-resource-route",{"title":333,"path":334,"stem":335,"children":336,"page":35},"Component Helpers","\u002Fnuxt-module\u002Fcomponent-helpers","5.nuxt-module\u002F7.component-helpers",[337,341,345,349,353],{"title":338,"path":339,"stem":340},"Images & Media","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fimages-and-uploads","5.nuxt-module\u002F7.component-helpers\u002F1.images-and-uploads",{"title":342,"path":343,"stem":344},"Collections & Pagination","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fcollections-and-pagination","5.nuxt-module\u002F7.component-helpers\u002F2.collections-and-pagination",{"title":346,"path":347,"stem":348},"HTML Content","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fhtml-content","5.nuxt-module\u002F7.component-helpers\u002F3.html-content",{"title":350,"path":351,"stem":352},"Real-Time Updates","\u002Fnuxt-module\u002Fcomponent-helpers\u002Freal-time-updates","5.nuxt-module\u002F7.component-helpers\u002F4.real-time-updates",{"title":237,"path":354,"stem":355},"\u002Fnuxt-module\u002Fcomponent-helpers\u002Fforms","5.nuxt-module\u002F7.component-helpers\u002F5.forms",{"title":357,"path":358,"stem":359},"Authentication","\u002Fnuxt-module\u002Fauthentication","5.nuxt-module\u002F8.authentication",{"title":361,"path":362,"stem":363,"children":364,"page":35},"Cwa Layer","\u002Fnuxt-module\u002Fcwa-layer","5.nuxt-module\u002F9.cwa-layer",[365,368,372,376,380],{"title":221,"path":366,"stem":367},"\u002Fnuxt-module\u002Fcwa-layer\u002Foverview","5.nuxt-module\u002F9.cwa-layer\u002F1.overview",{"title":369,"path":370,"stem":371},"Auth Pages","\u002Fnuxt-module\u002Fcwa-layer\u002Fauth-pages","5.nuxt-module\u002F9.cwa-layer\u002F2.auth-pages",{"title":373,"path":374,"stem":375},"Admin Panel","\u002Fnuxt-module\u002Fcwa-layer\u002Fadmin-panel","5.nuxt-module\u002F9.cwa-layer\u002F3.admin-panel",{"title":377,"path":378,"stem":379},"Styling","\u002Fnuxt-module\u002Fcwa-layer\u002Fstyling","5.nuxt-module\u002F9.cwa-layer\u002F4.styling",{"title":381,"path":382,"stem":383},"Date Pickers","\u002Fnuxt-module\u002Fcwa-layer\u002Fdate-pickers","5.nuxt-module\u002F9.cwa-layer\u002F5.date-pickers",{"title":385,"path":386,"stem":387,"children":388,"page":35},"Deployment","\u002Fdeployment","6.deployment",[389,393,397,401,405,409],{"title":390,"path":391,"stem":392},"Docker","\u002Fdeployment\u002Fdocker","6.deployment\u002F1.docker",{"title":394,"path":395,"stem":396},"Kubernetes & Helm","\u002Fdeployment\u002Fkubernetes","6.deployment\u002F2.kubernetes",{"title":398,"path":399,"stem":400},"CI\u002FCD","\u002Fdeployment\u002Fci-cd","6.deployment\u002F3.ci-cd",{"title":402,"path":403,"stem":404},"Load Testing","\u002Fdeployment\u002Fload-testing","6.deployment\u002F4.load-testing",{"title":406,"path":407,"stem":408},"Faster Functional Tests","\u002Fdeployment\u002Ffaster-functional-tests","6.deployment\u002F5.faster-functional-tests",{"title":410,"path":411,"stem":412},"Cloudflare","\u002Fdeployment\u002Fcloudflare","6.deployment\u002F6.cloudflare",{"id":414,"title":406,"badge":415,"body":416,"description":1987,"extension":1988,"links":415,"meta":1989,"navigation":654,"path":407,"seo":1990,"stem":408,"__hash__":1991},"docs\u002F6.deployment\u002F5.faster-functional-tests.md",null,{"type":417,"value":418,"toc":1979},"minimark",[419,428,440,461,466,469,517,521,527,606,620,627,1640,1647,1654,1660,1664,1671,1695,1698,1702,1721,1770,1787,1881,1884,1888,1891,1912,1916,1966,1969,1975],[420,421,422,423,427],"p",{},"The template's ",[424,425,426],"code",{},"functional tests"," job runs two tests. Almost all of its time is spent starting the container and Postgres, so there is nothing here to speed up. This page is for a project whose own suite has grown to hundreds of tests.",[420,429,430,431,435,436,439],{},"It is ",[432,433,434],"strong",{},"guidance, not a template default",". Sharding a small suite only adds runner cost, and GitLab's ",[424,437,438],{},"parallel:"," count is fixed in the YAML, so it can't size itself to the suite.",[441,442,444],"callout",{"icon":443},"i-heroicons-information-circle",[420,445,446,447,450,451,460],{},"The numbers on this page come from another CWA-based project with 211 Behat scenarios, where the job went from about ",[432,448,449],{},"568s to 81s",". The template replaced Behat with PHPUnit in ",[452,453,457],"a",{"href":454,"rel":455},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002F8885b3c01adafbe6e37263553d84ae9cd572b714",[456],"nofollow",[424,458,459],{},"8885b3c",", but the database reset costs the same SQL either way. Measure your own suite before you copy anything: the right fix depends on where your time goes.",[462,463,465],"h2",{"id":464},"measure-first","Measure first",[420,467,468],{},"Two numbers tell you which fix you need:",[470,471,472,491],"ul",{},[473,474,475,478,479,482,483,486,487,490],"li",{},[432,476,477],{},"The reset's share of the run."," Time ",[424,480,481],{},"FunctionalTestCase::setUp()"," in ",[424,484,485],{},"api\u002Ftests\u002FFunctional\u002FFunctionalTestCase.php"," and compare it with the whole suite. In that project, rebuilding the schema was ",[432,488,489],{},"66%"," of the run time, spread evenly across scenarios.",[473,492,493,496,497,500,501,504,505,508,509,512,513,516],{},[432,494,495],{},"The per-class distribution."," ",[424,498,499],{},"run_test_functional"," writes JUnit XML to ",[424,502,503],{},"api\u002Fbuild\u002Flogs\u002Fphpunit\u002Ffunctional.xml",". Each test class is a ",[424,506,507],{},"\u003Ctestsuite>"," and each test a ",[424,510,511],{},"\u003Ctestcase>",", both with a ",[424,514,515],{},"time"," attribute. If a few classes take most of the time, fix those classes instead.",[462,518,520],{"id":519},"reset-the-database-without-rebuilding-the-schema","Reset the database without rebuilding the schema",[420,522,422,523,526],{},[424,524,525],{},"FunctionalTestCase"," drops and recreates the whole schema before every test. That project measured four ways to reset:",[528,529,530,544],"table",{},[531,532,533],"thead",{},[534,535,536,540],"tr",{},[537,538,539],"th",{},"Reset",[537,541,543],{"align":542},"right","ms per scenario",[545,546,547,563,574,589],"tbody",{},[534,548,549,560],{},[550,551,552,555,556,559],"td",{},[424,553,554],{},"dropSchema"," + ",[424,557,558],{},"createSchema"," (the template)",[550,561,562],{"align":542},"756",[534,564,565,571],{},[550,566,567,568],{},"One ",[424,569,570],{},"TRUNCATE … RESTART IDENTITY CASCADE",[550,572,573],{"align":542},"185",[534,575,576,584],{},[550,577,578,482,581],{},[424,579,580],{},"ORMPurger",[424,582,583],{},"PURGE_MODE_TRUNCATE",[550,585,586],{"align":542},[432,587,588],{},"6,193",[534,590,591,601],{},[550,592,593],{},[432,594,595,482,597,600],{},[424,596,580],{},[424,598,599],{},"PURGE_MODE_DELETE",", plus a sequence reset",[550,602,603],{"align":542},[432,604,605],{},"22",[420,607,608,609,611,612,615,616,619],{},"Truncate mode is a trap: the purger runs one statement per table, and each is its own transaction. Delete mode deletes in Doctrine's commit order, so foreign keys need no special handling. ",[424,610,580],{}," comes from ",[424,613,614],{},"doctrine\u002Fdata-fixtures",", which the template already installs through ",[424,617,618],{},"doctrine\u002Fdoctrine-fixtures-bundle",".",[420,621,622,623,626],{},"Build the schema once, for the first test, then purge. Keep the template's check that the database name contains ",[424,624,625],{},"test",":",[628,629,634],"pre",{"className":630,"code":631,"filename":485,"language":632,"meta":633,"style":633},"language-php shiki shiki-themes github-light github-dark material-theme-palenight has-focused","\u003C?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Tests\\Functional;\n\nuse ApiPlatform\\Test\\ApiTestCase;\nuse Doctrine\\Common\\DataFixtures\\Purger\\ORMPurger;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Doctrine\\ORM\\Tools\\SchemaTool;\n\nabstract class FunctionalTestCase extends ApiTestCase\n{\n    private static bool $schemaCreated = false; \u002F\u002F [!code focus]\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        self::bootKernel();\n\n        $manager = self::getContainer()->get('doctrine')->getManager();\n        \\assert($manager instanceof EntityManagerInterface);\n        $connection = $manager->getConnection();\n        $database = $connection->getParams()['dbname'] ?? null;\n        if (!\\is_string($database) || !str_contains($database, 'test')) {\n            throw new \\RuntimeException(\\sprintf('Functional tests drop the schema. Refusing to run against \"%s\": point DATABASE_URL at a database whose name contains \"test\".', $database));\n        }\n\n        if (!self::$schemaCreated) { \u002F\u002F [!code focus]\n            $connection->executeStatement('CREATE EXTENSION IF NOT EXISTS citext');\n            $metadata = $manager->getMetadataFactory()->getAllMetadata();\n            $schemaTool = new SchemaTool($manager);\n            $schemaTool->dropSchema($metadata);\n            $schemaTool->createSchema($metadata);\n            self::$schemaCreated = true;\n        } else {\n            (new ORMPurger($manager))->purge(); \u002F\u002F PURGE_MODE_DELETE is the default \u002F\u002F [!code focus]\n            $this->restartSequences($manager); \u002F\u002F [!code focus]\n        }\n        $manager->clear();\n    }\n\n    private function restartSequences(EntityManagerInterface $manager): void\n    {\n        $connection = $manager->getConnection();\n        $sequences = $connection->fetchFirstColumn(\n            \"SELECT quote_ident(sequence_schema) || '.' || quote_ident(sequence_name)\n             FROM information_schema.sequences WHERE sequence_schema = current_schema()\"\n        );\n        foreach ($sequences as $sequence) {\n            $connection->executeStatement('ALTER SEQUENCE '.$sequence.' RESTART');\n        }\n    }\n}\n","php","",[424,635,636,649,656,680,685,711,716,738,767,786,809,814,833,839,868,873,894,900,915,928,933,982,1006,1028,1069,1122,1162,1168,1173,1199,1238,1266,1287,1306,1323,1342,1353,1384,1403,1408,1422,1428,1433,1457,1462,1481,1503,1526,1555,1561,1584,1624,1629,1634],{"__ignoreMap":633},[637,638,641,645],"span",{"class":639,"line":640},"line",1,[637,642,644],{"class":643},"sVlFx","\u003C?",[637,646,648],{"class":647},"sc2zw","php\n",[637,650,652],{"class":639,"line":651},2,[637,653,655],{"emptyLinePlaceholder":654},true,"\n",[637,657,659,663,667,670,673,677],{"class":639,"line":658},3,[637,660,662],{"class":661},"sm4w6","declare",[637,664,666],{"class":665},"sOvfz","(",[637,668,669],{"class":647},"strict_types",[637,671,672],{"class":643},"=",[637,674,676],{"class":675},"scSvc","1",[637,678,679],{"class":665},");\n",[637,681,683],{"class":639,"line":682},4,[637,684,655],{"emptyLinePlaceholder":654},[637,686,688,692,696,700,703,705,708],{"class":639,"line":687},5,[637,689,691],{"class":690},"stmX-","namespace",[637,693,695],{"class":694},"sRCss"," App",[637,697,699],{"class":698},"sn4go","\\",[637,701,702],{"class":694},"Tests",[637,704,699],{"class":698},[637,706,707],{"class":694},"Functional",[637,709,710],{"class":665},";\n",[637,712,714],{"class":639,"line":713},6,[637,715,655],{"emptyLinePlaceholder":654},[637,717,719,722,725,728,731,733,736],{"class":639,"line":718},7,[637,720,721],{"class":690},"use",[637,723,724],{"class":647}," ApiPlatform",[637,726,699],{"class":727},"sBtbT",[637,729,730],{"class":647},"Test",[637,732,699],{"class":727},[637,734,735],{"class":647},"ApiTestCase",[637,737,710],{"class":665},[637,739,741,743,746,748,751,753,756,758,761,763,765],{"class":639,"line":740},8,[637,742,721],{"class":690},[637,744,745],{"class":647}," Doctrine",[637,747,699],{"class":727},[637,749,750],{"class":647},"Common",[637,752,699],{"class":727},[637,754,755],{"class":647},"DataFixtures",[637,757,699],{"class":727},[637,759,760],{"class":647},"Purger",[637,762,699],{"class":727},[637,764,580],{"class":647},[637,766,710],{"class":665},[637,768,770,772,774,776,779,781,784],{"class":639,"line":769},9,[637,771,721],{"class":690},[637,773,745],{"class":647},[637,775,699],{"class":727},[637,777,778],{"class":647},"ORM",[637,780,699],{"class":727},[637,782,783],{"class":647},"EntityManagerInterface",[637,785,710],{"class":665},[637,787,789,791,793,795,797,799,802,804,807],{"class":639,"line":788},10,[637,790,721],{"class":690},[637,792,745],{"class":647},[637,794,699],{"class":727},[637,796,778],{"class":647},[637,798,699],{"class":727},[637,800,801],{"class":647},"Tools",[637,803,699],{"class":727},[637,805,806],{"class":647},"SchemaTool",[637,808,710],{"class":665},[637,810,812],{"class":639,"line":811},11,[637,813,655],{"emptyLinePlaceholder":654},[637,815,817,821,824,827,830],{"class":639,"line":816},12,[637,818,820],{"class":819},"swB56","abstract",[637,822,823],{"class":819}," class",[637,825,826],{"class":694}," FunctionalTestCase",[637,828,829],{"class":819}," extends",[637,831,832],{"class":694}," ApiTestCase\n",[637,834,836],{"class":639,"line":835},13,[637,837,838],{"class":665},"{\n",[637,840,844,847,850,853,856,860,862,865],{"class":841,"line":843},[639,842],"focused",14,[637,845,846],{"class":819},"    private",[637,848,849],{"class":819}," static",[637,851,852],{"class":690}," bool",[637,854,855],{"class":665}," $",[637,857,859],{"class":858},"sPB8G","schemaCreated ",[637,861,672],{"class":643},[637,863,864],{"class":727}," false",[637,866,867],{"class":665},";",[637,869,871],{"class":639,"line":870},15,[637,872,655],{"emptyLinePlaceholder":654},[637,874,876,879,882,886,889,891],{"class":639,"line":875},16,[637,877,878],{"class":819},"    protected",[637,880,881],{"class":819}," function",[637,883,885],{"class":884},"sKpYG"," setUp",[637,887,888],{"class":665},"()",[637,890,626],{"class":643},[637,892,893],{"class":690}," void\n",[637,895,897],{"class":639,"line":896},17,[637,898,899],{"class":665},"    {\n",[637,901,903,906,909,912],{"class":639,"line":902},18,[637,904,905],{"class":819},"        parent",[637,907,908],{"class":643},"::",[637,910,911],{"class":884},"setUp",[637,913,914],{"class":665},"();\n",[637,916,918,921,923,926],{"class":639,"line":917},19,[637,919,920],{"class":819},"        self",[637,922,908],{"class":643},[637,924,925],{"class":884},"bootKernel",[637,927,914],{"class":665},[637,929,931],{"class":639,"line":930},20,[637,932,655],{"emptyLinePlaceholder":654},[637,934,936,939,942,944,947,949,952,954,957,960,962,966,970,972,975,977,980],{"class":639,"line":935},21,[637,937,938],{"class":665},"        $",[637,940,941],{"class":858},"manager ",[637,943,672],{"class":643},[637,945,946],{"class":819}," self",[637,948,908],{"class":643},[637,950,951],{"class":884},"getContainer",[637,953,888],{"class":665},[637,955,956],{"class":643},"->",[637,958,959],{"class":884},"get",[637,961,666],{"class":665},[637,963,965],{"class":964},"seSrl","'",[637,967,969],{"class":968},"sLL54","doctrine",[637,971,965],{"class":964},[637,973,974],{"class":665},")",[637,976,956],{"class":643},[637,978,979],{"class":884},"getManager",[637,981,914],{"class":665},[637,983,985,988,992,995,997,1000,1004],{"class":639,"line":984},22,[637,986,987],{"class":727},"        \\",[637,989,991],{"class":990},"s3Ny6","assert",[637,993,994],{"class":665},"($",[637,996,941],{"class":858},[637,998,999],{"class":643},"instanceof",[637,1001,1003],{"class":1002},"sbW4m"," EntityManagerInterface",[637,1005,679],{"class":665},[637,1007,1009,1011,1014,1016,1018,1021,1023,1026],{"class":639,"line":1008},23,[637,1010,938],{"class":665},[637,1012,1013],{"class":858},"connection ",[637,1015,672],{"class":643},[637,1017,855],{"class":665},[637,1019,1020],{"class":858},"manager",[637,1022,956],{"class":643},[637,1024,1025],{"class":884},"getConnection",[637,1027,914],{"class":665},[637,1029,1031,1033,1036,1038,1040,1043,1045,1048,1051,1053,1056,1058,1061,1064,1067],{"class":639,"line":1030},24,[637,1032,938],{"class":665},[637,1034,1035],{"class":858},"database ",[637,1037,672],{"class":643},[637,1039,855],{"class":665},[637,1041,1042],{"class":858},"connection",[637,1044,956],{"class":643},[637,1046,1047],{"class":884},"getParams",[637,1049,1050],{"class":665},"()[",[637,1052,965],{"class":964},[637,1054,1055],{"class":968},"dbname",[637,1057,965],{"class":964},[637,1059,1060],{"class":665},"]",[637,1062,1063],{"class":643}," ??",[637,1065,1066],{"class":727}," null",[637,1068,710],{"class":665},[637,1070,1072,1075,1078,1081,1083,1086,1088,1091,1093,1096,1099,1102,1104,1106,1109,1112,1114,1116,1119],{"class":639,"line":1071},25,[637,1073,1074],{"class":661},"        if",[637,1076,1077],{"class":665}," (",[637,1079,1080],{"class":643},"!",[637,1082,699],{"class":727},[637,1084,1085],{"class":990},"is_string",[637,1087,994],{"class":665},[637,1089,1090],{"class":858},"database",[637,1092,974],{"class":665},[637,1094,1095],{"class":643}," ||",[637,1097,1098],{"class":643}," !",[637,1100,1101],{"class":990},"str_contains",[637,1103,994],{"class":665},[637,1105,1090],{"class":858},[637,1107,1108],{"class":665},",",[637,1110,1111],{"class":964}," '",[637,1113,625],{"class":968},[637,1115,965],{"class":964},[637,1117,1118],{"class":665},"))",[637,1120,1121],{"class":665}," {\n",[637,1123,1125,1128,1131,1134,1137,1139,1141,1144,1146,1148,1151,1153,1155,1157,1159],{"class":639,"line":1124},26,[637,1126,1127],{"class":661},"            throw",[637,1129,1130],{"class":690}," new",[637,1132,1133],{"class":727}," \\",[637,1135,1136],{"class":1002},"RuntimeException",[637,1138,666],{"class":665},[637,1140,699],{"class":727},[637,1142,1143],{"class":990},"sprintf",[637,1145,666],{"class":665},[637,1147,965],{"class":964},[637,1149,1150],{"class":968},"Functional tests drop the schema. Refusing to run against \"%s\": point DATABASE_URL at a database whose name contains \"test\".",[637,1152,965],{"class":964},[637,1154,1108],{"class":665},[637,1156,855],{"class":665},[637,1158,1090],{"class":858},[637,1160,1161],{"class":665},"));\n",[637,1163,1165],{"class":639,"line":1164},27,[637,1166,1167],{"class":665},"        }\n",[637,1169,1171],{"class":639,"line":1170},28,[637,1172,655],{"emptyLinePlaceholder":654},[637,1174,1177,1179,1181,1183,1186,1188,1191,1194,1196],{"class":1175,"line":1176},[639,842],29,[637,1178,1074],{"class":661},[637,1180,1077],{"class":665},[637,1182,1080],{"class":643},[637,1184,1185],{"class":819},"self",[637,1187,908],{"class":643},[637,1189,1190],{"class":665},"$",[637,1192,1193],{"class":858},"schemaCreated",[637,1195,974],{"class":665},[637,1197,1198],{"class":665}," {",[637,1200,1202,1205,1207,1209,1212,1214,1216,1219,1222,1225,1228,1231,1234,1236],{"class":639,"line":1201},30,[637,1203,1204],{"class":665},"            $",[637,1206,1042],{"class":858},[637,1208,956],{"class":643},[637,1210,1211],{"class":884},"executeStatement",[637,1213,666],{"class":665},[637,1215,965],{"class":964},[637,1217,1218],{"class":690},"CREATE",[637,1220,1221],{"class":968}," EXTENSION ",[637,1223,1224],{"class":690},"IF",[637,1226,1227],{"class":690}," NOT",[637,1229,1230],{"class":690}," EXISTS",[637,1232,1233],{"class":968}," citext",[637,1235,965],{"class":964},[637,1237,679],{"class":665},[637,1239,1241,1243,1246,1248,1250,1252,1254,1257,1259,1261,1264],{"class":639,"line":1240},31,[637,1242,1204],{"class":665},[637,1244,1245],{"class":858},"metadata ",[637,1247,672],{"class":643},[637,1249,855],{"class":665},[637,1251,1020],{"class":858},[637,1253,956],{"class":643},[637,1255,1256],{"class":884},"getMetadataFactory",[637,1258,888],{"class":665},[637,1260,956],{"class":643},[637,1262,1263],{"class":884},"getAllMetadata",[637,1265,914],{"class":665},[637,1267,1269,1271,1274,1276,1278,1281,1283,1285],{"class":639,"line":1268},32,[637,1270,1204],{"class":665},[637,1272,1273],{"class":858},"schemaTool ",[637,1275,672],{"class":643},[637,1277,1130],{"class":690},[637,1279,1280],{"class":1002}," SchemaTool",[637,1282,994],{"class":665},[637,1284,1020],{"class":858},[637,1286,679],{"class":665},[637,1288,1290,1292,1295,1297,1299,1301,1304],{"class":639,"line":1289},33,[637,1291,1204],{"class":665},[637,1293,1294],{"class":858},"schemaTool",[637,1296,956],{"class":643},[637,1298,554],{"class":884},[637,1300,994],{"class":665},[637,1302,1303],{"class":858},"metadata",[637,1305,679],{"class":665},[637,1307,1309,1311,1313,1315,1317,1319,1321],{"class":639,"line":1308},34,[637,1310,1204],{"class":665},[637,1312,1294],{"class":858},[637,1314,956],{"class":643},[637,1316,558],{"class":884},[637,1318,994],{"class":665},[637,1320,1303],{"class":858},[637,1322,679],{"class":665},[637,1324,1326,1329,1331,1333,1335,1337,1340],{"class":639,"line":1325},35,[637,1327,1328],{"class":819},"            self",[637,1330,908],{"class":643},[637,1332,1190],{"class":665},[637,1334,859],{"class":858},[637,1336,672],{"class":643},[637,1338,1339],{"class":727}," true",[637,1341,710],{"class":665},[637,1343,1345,1348,1351],{"class":639,"line":1344},36,[637,1346,1347],{"class":665},"        }",[637,1349,1350],{"class":661}," else",[637,1352,1121],{"class":665},[637,1354,1357,1360,1363,1366,1368,1370,1372,1374,1377,1380],{"class":1355,"line":1356},[639,842],37,[637,1358,1359],{"class":665},"            (",[637,1361,1362],{"class":690},"new",[637,1364,1365],{"class":1002}," ORMPurger",[637,1367,994],{"class":665},[637,1369,1020],{"class":858},[637,1371,1118],{"class":665},[637,1373,956],{"class":643},[637,1375,1376],{"class":884},"purge",[637,1378,1379],{"class":665},"();",[637,1381,1383],{"class":1382},"sTBSN"," \u002F\u002F PURGE_MODE_DELETE is the default",[637,1385,1388,1391,1393,1396,1398,1400],{"class":1386,"line":1387},[639,842],38,[637,1389,1390],{"class":727},"            $this",[637,1392,956],{"class":643},[637,1394,1395],{"class":884},"restartSequences",[637,1397,994],{"class":665},[637,1399,1020],{"class":858},[637,1401,1402],{"class":665},");",[637,1404,1406],{"class":639,"line":1405},39,[637,1407,1167],{"class":665},[637,1409,1411,1413,1415,1417,1420],{"class":639,"line":1410},40,[637,1412,938],{"class":665},[637,1414,1020],{"class":858},[637,1416,956],{"class":643},[637,1418,1419],{"class":884},"clear",[637,1421,914],{"class":665},[637,1423,1425],{"class":639,"line":1424},41,[637,1426,1427],{"class":665},"    }\n",[637,1429,1431],{"class":639,"line":1430},42,[637,1432,655],{"emptyLinePlaceholder":654},[637,1434,1436,1438,1440,1443,1445,1447,1449,1451,1453,1455],{"class":639,"line":1435},43,[637,1437,846],{"class":819},[637,1439,881],{"class":819},[637,1441,1442],{"class":884}," restartSequences",[637,1444,666],{"class":665},[637,1446,783],{"class":1002},[637,1448,855],{"class":665},[637,1450,1020],{"class":858},[637,1452,974],{"class":665},[637,1454,626],{"class":643},[637,1456,893],{"class":690},[637,1458,1460],{"class":639,"line":1459},44,[637,1461,899],{"class":665},[637,1463,1465,1467,1469,1471,1473,1475,1477,1479],{"class":639,"line":1464},45,[637,1466,938],{"class":665},[637,1468,1013],{"class":858},[637,1470,672],{"class":643},[637,1472,855],{"class":665},[637,1474,1020],{"class":858},[637,1476,956],{"class":643},[637,1478,1025],{"class":884},[637,1480,914],{"class":665},[637,1482,1484,1486,1489,1491,1493,1495,1497,1500],{"class":639,"line":1483},46,[637,1485,938],{"class":665},[637,1487,1488],{"class":858},"sequences ",[637,1490,672],{"class":643},[637,1492,855],{"class":665},[637,1494,1042],{"class":858},[637,1496,956],{"class":643},[637,1498,1499],{"class":884},"fetchFirstColumn",[637,1501,1502],{"class":665},"(\n",[637,1504,1506,1509,1512,1515,1518,1521,1523],{"class":639,"line":1505},47,[637,1507,1508],{"class":964},"            \"",[637,1510,1511],{"class":690},"SELECT",[637,1513,1514],{"class":968}," quote_ident(sequence_schema) ",[637,1516,1517],{"class":643},"||",[637,1519,1520],{"class":968}," '.' ",[637,1522,1517],{"class":643},[637,1524,1525],{"class":968}," quote_ident(sequence_name)\n",[637,1527,1529,1532,1536,1538,1541,1544,1547,1549,1552],{"class":639,"line":1528},48,[637,1530,1531],{"class":690},"             FROM",[637,1533,1535],{"class":1534},"szhYu"," information_schema",[637,1537,619],{"class":968},[637,1539,1540],{"class":1534},"sequences",[637,1542,1543],{"class":690}," WHERE",[637,1545,1546],{"class":968}," sequence_schema ",[637,1548,672],{"class":643},[637,1550,1551],{"class":968}," current_schema",[637,1553,1554],{"class":964},"()\"\n",[637,1556,1558],{"class":639,"line":1557},49,[637,1559,1560],{"class":665},"        );\n",[637,1562,1564,1567,1570,1572,1575,1577,1580,1582],{"class":639,"line":1563},50,[637,1565,1566],{"class":661},"        foreach",[637,1568,1569],{"class":665}," ($",[637,1571,1488],{"class":858},[637,1573,1574],{"class":643},"as",[637,1576,855],{"class":665},[637,1578,1579],{"class":858},"sequence",[637,1581,974],{"class":665},[637,1583,1121],{"class":665},[637,1585,1587,1589,1591,1593,1595,1597,1599,1602,1605,1607,1609,1611,1613,1615,1617,1620,1622],{"class":639,"line":1586},51,[637,1588,1204],{"class":665},[637,1590,1042],{"class":858},[637,1592,956],{"class":643},[637,1594,1211],{"class":884},[637,1596,666],{"class":665},[637,1598,965],{"class":964},[637,1600,1601],{"class":690},"ALTER",[637,1603,1604],{"class":690}," SEQUENCE",[637,1606,1111],{"class":964},[637,1608,619],{"class":643},[637,1610,1190],{"class":665},[637,1612,1579],{"class":858},[637,1614,619],{"class":643},[637,1616,965],{"class":964},[637,1618,1619],{"class":968}," RESTART",[637,1621,965],{"class":964},[637,1623,679],{"class":665},[637,1625,1627],{"class":639,"line":1626},52,[637,1628,1167],{"class":665},[637,1630,1632],{"class":639,"line":1631},53,[637,1633,1427],{"class":665},[637,1635,1637],{"class":639,"line":1636},54,[637,1638,1639],{"class":665},"}\n",[420,1641,1642,1643,1646],{},"The static flag lives for the whole PHPUnit process, so each run (and each shard) builds the schema exactly once. Don't turn on PHPUnit's ",[424,1644,1645],{},"backupStaticProperties",", which would reset it.",[420,1648,1649,1650,1653],{},"That project also tried ",[424,1651,1652],{},"DAMA\\DoctrineTestBundle",", which wraps each test in a rolled-back transaction. It was a new dependency for about 3% more, and it doesn't roll back sequences.",[441,1655,1657],{"icon":1656},"i-heroicons-photo",[420,1658,1659],{},"Image placeholder: a bar chart of the four reset strategies in ms per scenario, with the truncate-mode purger standing out.",[462,1661,1663],{"id":1662},"the-sequence-trap","The sequence trap",[420,1665,1666,1667,1670],{},"A delete leaves sequences where they were, so a purged database hands out new ids. Any test that asserts an IRI such as ",[424,1668,1669],{},"\u002F_api\u002F…\u002F1"," then fails, depending on which tests ran before it.",[470,1672,1673,1684],{},[473,1674,1675,1676,1679,1680,1683],{},"A sequence that Doctrine creates for the ",[424,1677,1678],{},"SEQUENCE"," id strategy is a standalone sequence that no column owns. ",[424,1681,1682],{},"TRUNCATE … RESTART IDENTITY"," only restarts sequences owned by the truncated tables' columns, so it leaves these alone and the ids drift. That project hit exactly this.",[473,1685,1686,1687,1690,1691,1694],{},"Only an explicit ",[424,1688,1689],{},"ALTER SEQUENCE … RESTART"," for each sequence, as in ",[424,1692,1693],{},"restartSequences()"," above, keeps ids stable with either reset.",[420,1696,1697],{},"CWA's own entities use UUIDs, and the template's entities do too, so a project with no integer ids has no sequences to restart.",[462,1699,1701],{"id":1700},"shard-the-job-by-test","Shard the job by test",[420,1703,1704,1705,1708,1709,1712,1713,1716,1717,1720],{},"Once the reset is cheap, the fixed cost of each job dominates. At that point, split the suite across parallel jobs with GitLab's ",[424,1706,1707],{},"parallel: N",". Each copy of the job gets its own ",[424,1710,1711],{},"postgres"," service, and ",[424,1714,1715],{},"CI_NODE_INDEX"," (from 1) and ",[424,1718,1719],{},"CI_NODE_TOTAL"," tell it which share to run:",[628,1722,1727],{"className":1723,"code":1724,"filename":1725,"language":1726,"meta":633,"style":633},"language-yaml shiki shiki-themes github-light github-dark material-theme-palenight has-focused","functional tests:\n  parallel: 4 # [!code focus]\n  script:\n    - setup_test_db_environment\n    - run_test_functional\n",".gitlab-ci.yml","yaml",[424,1728,1729,1737,1748,1755,1763],{"__ignoreMap":633},[637,1730,1731,1734],{"class":639,"line":640},[637,1732,426],{"class":1733},"s-h7I",[637,1735,1736],{"class":665},":\n",[637,1738,1740,1743,1745],{"class":1739,"line":651},[639,842],[637,1741,1742],{"class":1733},"  parallel",[637,1744,626],{"class":665},[637,1746,1747],{"class":675}," 4",[637,1749,1750,1753],{"class":639,"line":658},[637,1751,1752],{"class":1733},"  script",[637,1754,1736],{"class":665},[637,1756,1757,1760],{"class":639,"line":682},[637,1758,1759],{"class":665},"    -",[637,1761,1762],{"class":968}," setup_test_db_environment\n",[637,1764,1765,1767],{"class":639,"line":687},[637,1766,1759],{"class":665},[637,1768,1769],{"class":968}," run_test_functional\n",[420,1771,1772,1773,1776,1777,1780,1781,1783,1784,619],{},"PHPUnit has no built-in sharding, but it can list the suite and run part of it. ",[424,1774,1775],{},"vendor\u002Fbin\u002Fphpunit tests\u002FFunctional --list-test-ids"," prints one ID per test, and ",[424,1778,1779],{},"--test-id-filter-file \u003Cfile>"," runs only the IDs in a file, one per line. Your shard script writes this job's share to a file, and ",[424,1782,499],{}," passes it to ",[424,1785,1786],{},"phpunit",[470,1788,1789,1795,1858,1864],{},[473,1790,1791,1794],{},[432,1792,1793],{},"Shard by test, not by class."," In that project one feature file held 100 of the 211 scenarios, so no split by file could balance it. Weight each test by its time in an earlier JUnit report (a data provider counts each data set), and assign the heaviest first, each to the lightest shard so far.",[473,1796,1797,1800,1801],{},[432,1798,1799],{},"An empty shard must exit 0 without starting PHPUnit."," Then a shard that got nothing can't run the whole suite by mistake:",[628,1802,1806],{"className":1803,"code":1804,"language":1805,"meta":633,"style":633},"language-sh shiki shiki-themes github-light github-dark material-theme-palenight","[ -s \"$SHARD_FILE\" ] || { echo \"No tests for shard $CI_NODE_INDEX\"; exit 0; }\n","sh",[424,1807,1808],{"__ignoreMap":633},[637,1809,1810,1813,1816,1819,1822,1825,1828,1830,1832,1835,1837,1840,1843,1845,1847,1850,1853,1855],{"class":639,"line":640},[637,1811,1812],{"class":665},"[",[637,1814,1815],{"class":643}," -s",[637,1817,1818],{"class":964}," \"",[637,1820,1821],{"class":858},"$SHARD_FILE",[637,1823,1824],{"class":964},"\"",[637,1826,1827],{"class":665}," ]",[637,1829,1095],{"class":643},[637,1831,1198],{"class":665},[637,1833,1834],{"class":990}," echo",[637,1836,1818],{"class":964},[637,1838,1839],{"class":968},"No tests for shard ",[637,1841,1842],{"class":858},"$CI_NODE_INDEX",[637,1844,1824],{"class":964},[637,1846,867],{"class":665},[637,1848,1849],{"class":990}," exit",[637,1851,1852],{"class":675}," 0",[637,1854,867],{"class":665},[637,1856,1857],{"class":665}," }\n",[473,1859,1860,1863],{},[432,1861,1862],{},"Choose N from the fixed cost per job."," It was about 81s there, so extra shards soon cost more runner minutes than they save. N=4 was the sweet spot. N=8 doubled the runner minutes to save about 55s.",[473,1865,1866,1869,1870,1872,1873,1876,1877,1880],{},[432,1867,1868],{},"Share one vendor cache across shards."," The template's ",[424,1871,426],{}," job runs ",[424,1874,1875],{},"composer install"," and has no cache. If you add one, key it on ",[424,1878,1879],{},"api\u002Fcomposer.lock",". A key that includes the job name or branch makes every shard of a new branch start cold.",[420,1882,1883],{},"Give each shard its own JUnit file name. GitLab merges the reports of parallel jobs in the test report.",[462,1885,1887],{"id":1886},"prove-the-order-doesnt-matter","Prove the order doesn't matter",[420,1889,1890],{},"A reset that leaves anything behind makes a test depend on the ones before it. Sharding changes that order, so check it before you trust the shards. Run the full suite:",[470,1892,1893,1896,1902],{},[473,1894,1895],{},"in its default order,",[473,1897,1898,1899,1108],{},"with ",[424,1900,1901],{},"--order-by reverse",[473,1903,1898,1904,1907,1908,1911],{},[424,1905,1906],{},"--order-by random"," under several ",[424,1909,1910],{},"--random-order-seed"," values.",[462,1913,1915],{"id":1914},"results-in-that-project","Results in that project",[528,1917,1918,1928],{},[531,1919,1920],{},[534,1921,1922,1925],{},[537,1923,1924],{},"Change",[537,1926,1927],{"align":542},"Job time",[545,1929,1930,1938,1946,1954],{},[534,1931,1932,1935],{},[550,1933,1934],{},"Before",[550,1936,1937],{"align":542},"about 568s",[534,1939,1940,1943],{},[550,1941,1942],{},"Sharding alone",[550,1944,1945],{"align":542},"191s",[534,1947,1948,1951],{},[550,1949,1950],{},"The reset alone",[550,1952,1953],{"align":542},"181s",[534,1955,1956,1961],{},[550,1957,1958],{},[432,1959,1960],{},"Both",[550,1962,1963],{"align":542},[432,1964,1965],{},"81s",[420,1967,1968],{},"The runner minutes roughly halved as well.",[420,1970,1971,1972,1974],{},"See ",[452,1973,398],{"href":399}," for the rest of the pipeline.",[1976,1977,1978],"style",{},"html pre.shiki code .sVlFx, html code.shiki .sVlFx{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#89DDFF}html pre.shiki code .sc2zw, html code.shiki .sc2zw{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#BABED8}html pre.shiki code .sm4w6, html code.shiki .sm4w6{--shiki-light:#D73A49;--shiki-light-font-style:inherit;--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sOvfz, html code.shiki .sOvfz{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#89DDFF}html pre.shiki code .scSvc, html code.shiki .scSvc{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#F78C6C}html pre.shiki code .stmX-, html code.shiki .stmX-{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F78C6C}html pre.shiki code .sRCss, html code.shiki .sRCss{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#FFCB6B}html pre.shiki code .sn4go, html code.shiki .sn4go{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#89DDFF}html pre.shiki code .sBtbT, html code.shiki .sBtbT{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#89DDFF}html pre.shiki code .swB56, html code.shiki .swB56{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#C792EA}html pre.shiki code .sPB8G, html code.shiki .sPB8G{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#BABED8}html pre.shiki code .sKpYG, html code.shiki .sKpYG{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#82AAFF}html pre.shiki code .seSrl, html code.shiki .seSrl{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#89DDFF}html pre.shiki code .sLL54, html code.shiki .sLL54{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#C3E88D}html pre.shiki code .s3Ny6, html code.shiki .s3Ny6{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#82AAFF}html pre.shiki code .sbW4m, html code.shiki .sbW4m{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#FFCB6B}html pre.shiki code .sTBSN, html code.shiki .sTBSN{--shiki-light:#6A737D;--shiki-light-font-style:inherit;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .szhYu, html code.shiki .szhYu{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-h7I, html code.shiki .s-h7I{--shiki-light:#22863A;--shiki-default:#85E89D;--shiki-dark:#F07178}",{"title":633,"searchDepth":651,"depth":651,"links":1980},[1981,1982,1983,1984,1985,1986],{"id":464,"depth":651,"text":465},{"id":519,"depth":651,"text":520},{"id":1662,"depth":651,"text":1663},{"id":1700,"depth":651,"text":1701},{"id":1886,"depth":651,"text":1887},{"id":1914,"depth":651,"text":1915},"When a project's functional test suite slows CI down — measure where the time goes, reset the database without rebuilding the schema, and shard the job by test.","md",{},{"title":406,"description":1987},"Hy5r7tTyaymBfRJITUskkIEqDSa0Bi9wJ6v50ZzhZDA",[1993,1995],{"title":402,"path":403,"stem":404,"description":1994,"children":-1},"Stress test a CWA site with the template's k6 script — how many visitors it serves, how quickly, and whether they got the page cache or a server-side render.",{"title":410,"path":411,"stem":412,"description":1996,"children":-1},"Putting a CWA site behind Cloudflare. Proxy only is the recommended default. Edge caching has been tested end to end on the Free plan and is a supported choice, with an optional second rule for API responses.",1791209736630]