Merge pull request #30232 from overleaf/ar/convert-clsi-to-es-modules
[clsi] convert to ES modules GitOrigin-RevId: fb7fa52cc8f678ee31be352e62a5dff95e88008b
This commit is contained in:
Generated
+855
-2
@@ -55085,11 +55085,11 @@
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"mock-fs": "^5.1.2",
|
||||
"node-fetch": "^2.7.0",
|
||||
"sandboxed-module": "^2.0.4",
|
||||
"sinon": "~9.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"timekeeper": "2.2.0",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"services/clsi-cache": {
|
||||
@@ -55264,6 +55264,585 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/expect": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz",
|
||||
"integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/expect/node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/mocker": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz",
|
||||
"integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.16",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz",
|
||||
"integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/runner": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz",
|
||||
"integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.16",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz",
|
||||
"integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/spy": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz",
|
||||
"integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/@vitest/utils": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz",
|
||||
"integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -55273,6 +55852,89 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/esbuild": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.2",
|
||||
"@esbuild/android-arm": "0.27.2",
|
||||
"@esbuild/android-arm64": "0.27.2",
|
||||
"@esbuild/android-x64": "0.27.2",
|
||||
"@esbuild/darwin-arm64": "0.27.2",
|
||||
"@esbuild/darwin-x64": "0.27.2",
|
||||
"@esbuild/freebsd-arm64": "0.27.2",
|
||||
"@esbuild/freebsd-x64": "0.27.2",
|
||||
"@esbuild/linux-arm": "0.27.2",
|
||||
"@esbuild/linux-arm64": "0.27.2",
|
||||
"@esbuild/linux-ia32": "0.27.2",
|
||||
"@esbuild/linux-loong64": "0.27.2",
|
||||
"@esbuild/linux-mips64el": "0.27.2",
|
||||
"@esbuild/linux-ppc64": "0.27.2",
|
||||
"@esbuild/linux-riscv64": "0.27.2",
|
||||
"@esbuild/linux-s390x": "0.27.2",
|
||||
"@esbuild/linux-x64": "0.27.2",
|
||||
"@esbuild/netbsd-arm64": "0.27.2",
|
||||
"@esbuild/netbsd-x64": "0.27.2",
|
||||
"@esbuild/openbsd-arm64": "0.27.2",
|
||||
"@esbuild/openbsd-x64": "0.27.2",
|
||||
"@esbuild/openharmony-arm64": "0.27.2",
|
||||
"@esbuild/sunos-x64": "0.27.2",
|
||||
"@esbuild/win32-arm64": "0.27.2",
|
||||
"@esbuild/win32-ia32": "0.27.2",
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/sinon": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.3.tgz",
|
||||
@@ -55304,6 +55966,197 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/tinyexec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/tinyrainbow": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/vitest": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/runner": "4.0.16",
|
||||
"@vitest/snapshot": "4.0.16",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^3.10.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"vite": "^6.0.0 || ^7.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.16",
|
||||
"@vitest/browser-preview": "4.0.16",
|
||||
"@vitest/browser-webdriverio": "4.0.16",
|
||||
"@vitest/ui": "4.0.16",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"services/clsi/node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"services/contacts": {
|
||||
"name": "@overleaf/contacts",
|
||||
"dependencies": {
|
||||
|
||||
+29
-31
@@ -1,36 +1,39 @@
|
||||
// Metrics must be initialized before importing anything else
|
||||
require('@overleaf/metrics/initialize')
|
||||
import '@overleaf/metrics/initialize.js'
|
||||
|
||||
const CompileController = require('./app/js/CompileController')
|
||||
const ContentController = require('./app/js/ContentController')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
import CompileController from './app/js/CompileController.js'
|
||||
import ContentController from './app/js/ContentController.js'
|
||||
import Settings from '@overleaf/settings'
|
||||
import logger from '@overleaf/logger'
|
||||
import LoggerSerializers from './app/js/LoggerSerializers.js'
|
||||
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import smokeTest from './test/smoke/js/SmokeTests.js'
|
||||
import ContentTypeMapper from './app/js/ContentTypeMapper.js'
|
||||
import Errors from './app/js/Errors.js'
|
||||
import OutputController from './app/js/OutputController.js'
|
||||
import Path from 'node:path'
|
||||
|
||||
import ProjectPersistenceManager from './app/js/ProjectPersistenceManager.js'
|
||||
import OutputCacheManager from './app/js/OutputCacheManager.js'
|
||||
import ContentCacheManager from './app/js/ContentCacheManager.js'
|
||||
|
||||
import express from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
|
||||
import ForbidSymlinks from './app/js/StaticServerForbidSymlinks.js'
|
||||
|
||||
import net from 'node:net'
|
||||
import os from 'node:os'
|
||||
logger.initialize('clsi')
|
||||
const LoggerSerializers = require('./app/js/LoggerSerializers')
|
||||
logger.logger.serializers.clsiRequest = LoggerSerializers.clsiRequest
|
||||
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
|
||||
const smokeTest = require('./test/smoke/js/SmokeTests')
|
||||
const ContentTypeMapper = require('./app/js/ContentTypeMapper')
|
||||
const Errors = require('./app/js/Errors')
|
||||
const { createOutputZip } = require('./app/js/OutputController')
|
||||
|
||||
const Path = require('node:path')
|
||||
|
||||
Metrics.open_sockets.monitor(true)
|
||||
Metrics.memory.monitor(logger)
|
||||
Metrics.leaked_sockets.monitor(logger)
|
||||
|
||||
const ProjectPersistenceManager = require('./app/js/ProjectPersistenceManager')
|
||||
const OutputCacheManager = require('./app/js/OutputCacheManager')
|
||||
const ContentCacheManager = require('./app/js/ContentCacheManager')
|
||||
|
||||
ProjectPersistenceManager.init()
|
||||
OutputCacheManager.init()
|
||||
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
const app = express()
|
||||
|
||||
Metrics.injectMetricsRoute(app)
|
||||
@@ -126,8 +129,6 @@ app.get(
|
||||
CompileController.wordcount
|
||||
)
|
||||
|
||||
const ForbidSymlinks = require('./app/js/StaticServerForbidSymlinks')
|
||||
|
||||
// create a static server which does not allow access to any symlinks
|
||||
// avoids possible mismatch of root directory between middleware check
|
||||
// and serving the files
|
||||
@@ -155,14 +156,14 @@ const staticOutputServer = ForbidSymlinks(
|
||||
app.get(
|
||||
'/project/:project_id/build/:build_id/output/output.zip',
|
||||
bodyParser.json(),
|
||||
createOutputZip
|
||||
OutputController.createOutputZip
|
||||
)
|
||||
|
||||
// This needs to be before GET /project/:project_id/user/:user_id/build/:build_id/output/*
|
||||
app.get(
|
||||
'/project/:project_id/user/:user_id/build/:build_id/output/output.zip',
|
||||
bodyParser.json(),
|
||||
createOutputZip
|
||||
OutputController.createOutputZip
|
||||
)
|
||||
|
||||
app.get(
|
||||
@@ -275,9 +276,6 @@ app.use(function (error, req, res, next) {
|
||||
}
|
||||
})
|
||||
|
||||
const net = require('node:net')
|
||||
const os = require('node:os')
|
||||
|
||||
let STATE = 'up'
|
||||
|
||||
const loadTcpServer = net.createServer(function (socket) {
|
||||
@@ -360,7 +358,7 @@ const host = Settings.internal.clsi.host
|
||||
const loadTcpPort = Settings.internal.load_balancer_agent.load_port
|
||||
const loadHttpPort = Settings.internal.load_balancer_agent.local_port
|
||||
|
||||
if (!module.parent) {
|
||||
if (import.meta.main) {
|
||||
// Called directly
|
||||
|
||||
// handle uncaught exceptions when running in production
|
||||
@@ -394,4 +392,4 @@ if (!module.parent) {
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = app
|
||||
export default app
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
// @ts-check
|
||||
const crypto = require('node:crypto')
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const { pipeline } = require('node:stream/promises')
|
||||
const { createGzip, createGunzip } = require('node:zlib')
|
||||
const { crc32 } = require('node:zlib')
|
||||
const tarFs = require('tar-fs')
|
||||
const _ = require('lodash')
|
||||
const {
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import Path from 'node:path'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { crc32, createGzip, createGunzip } from 'node:zlib'
|
||||
import tarFs from 'tar-fs'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
fetchNothing,
|
||||
fetchStream,
|
||||
RequestFailedError,
|
||||
} = require('@overleaf/fetch-utils')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const { MeteredStream } = require('@overleaf/stream-utils')
|
||||
const { CACHE_SUBDIR } = require('./OutputCacheManager')
|
||||
const { isExtraneousFile } = require('./ResourceWriter')
|
||||
} from '@overleaf/fetch-utils'
|
||||
import logger from '@overleaf/logger'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import Settings from '@overleaf/settings'
|
||||
import { MeteredStream } from '@overleaf/stream-utils'
|
||||
import OutputCacheManager from './OutputCacheManager.js'
|
||||
import ResourceWriter from './ResourceWriter.js'
|
||||
|
||||
const { CACHE_SUBDIR } = OutputCacheManager
|
||||
const { isExtraneousFile } = ResourceWriter
|
||||
|
||||
const TIMEOUT = 5_000
|
||||
/**
|
||||
@@ -426,7 +428,7 @@ async function downloadLatestCompileCache(projectId, userId, compileDir) {
|
||||
return !abort
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
notifyCLSICacheAboutBuild,
|
||||
downloadLatestCompileCache,
|
||||
downloadOutputDotSynctexFromCompileCache,
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import Settings from '@overleaf/settings'
|
||||
import logger from '@overleaf/logger'
|
||||
let commandRunnerPath
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
|
||||
if ((Settings.clsi != null ? Settings.clsi.dockerRunner : undefined) === true) {
|
||||
commandRunnerPath = './DockerRunner'
|
||||
commandRunnerPath = './DockerRunner.js'
|
||||
} else {
|
||||
commandRunnerPath = './LocalCommandRunner'
|
||||
commandRunnerPath = './LocalCommandRunner.js'
|
||||
}
|
||||
logger.debug({ commandRunnerPath }, 'selecting command runner for clsi')
|
||||
const CommandRunner = require(commandRunnerPath)
|
||||
const CommandRunner = (await import(commandRunnerPath)).default
|
||||
|
||||
module.exports = CommandRunner
|
||||
export default CommandRunner
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
const Path = require('node:path')
|
||||
const RequestParser = require('./RequestParser')
|
||||
const CompileManager = require('./CompileManager')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const ProjectPersistenceManager = require('./ProjectPersistenceManager')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Errors = require('./Errors')
|
||||
const { notifyCLSICacheAboutBuild } = require('./CLSICacheHandler')
|
||||
import Path from 'node:path'
|
||||
import RequestParser from './RequestParser.js'
|
||||
import CompileManager from './CompileManager.js'
|
||||
import Settings from '@overleaf/settings'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import ProjectPersistenceManager from './ProjectPersistenceManager.js'
|
||||
import logger from '@overleaf/logger'
|
||||
import Errors from './Errors.js'
|
||||
import CLSICacheHandler from './CLSICacheHandler.js'
|
||||
|
||||
const { notifyCLSICacheAboutBuild } = CLSICacheHandler
|
||||
|
||||
let lastSuccessfulCompileTimestamp = 0
|
||||
|
||||
@@ -272,7 +274,7 @@ function status(req, res, next) {
|
||||
res.send('OK')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
compile,
|
||||
stopCompile,
|
||||
clearCache,
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
const fsPromises = require('node:fs/promises')
|
||||
const os = require('node:os')
|
||||
const Path = require('node:path')
|
||||
const { callbackify } = require('node:util')
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import Path from 'node:path'
|
||||
import { callbackify } from 'node:util'
|
||||
import Settings from '@overleaf/settings'
|
||||
import logger from '@overleaf/logger'
|
||||
import OError from '@overleaf/o-error'
|
||||
import ResourceWriter from './ResourceWriter.js'
|
||||
import LatexRunner from './LatexRunner.js'
|
||||
import OutputFileFinder from './OutputFileFinder.js'
|
||||
import OutputCacheManager from './OutputCacheManager.js'
|
||||
import ClsiMetrics from './Metrics.js'
|
||||
import DraftModeManager from './DraftModeManager.js'
|
||||
import TikzManager from './TikzManager.js'
|
||||
import LockManager from './LockManager.js'
|
||||
import Errors from './Errors.js'
|
||||
import CommandRunner from './CommandRunner.js'
|
||||
import ContentCacheMetrics from './ContentCacheMetrics.js'
|
||||
import SynctexOutputParser from './SynctexOutputParser.js'
|
||||
import CLSICacheHandler from './CLSICacheHandler.js'
|
||||
import StatsManager from './StatsManager.js'
|
||||
import SafeReader from './SafeReader.js'
|
||||
import LatexMetrics from './LatexMetrics.js'
|
||||
import { callbackifyMultiResult } from '@overleaf/promise-utils'
|
||||
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
const OError = require('@overleaf/o-error')
|
||||
|
||||
const ResourceWriter = require('./ResourceWriter')
|
||||
const LatexRunner = require('./LatexRunner')
|
||||
const OutputFileFinder = require('./OutputFileFinder')
|
||||
const OutputCacheManager = require('./OutputCacheManager')
|
||||
const ClsiMetrics = require('./Metrics')
|
||||
const DraftModeManager = require('./DraftModeManager')
|
||||
const TikzManager = require('./TikzManager')
|
||||
const LockManager = require('./LockManager')
|
||||
const Errors = require('./Errors')
|
||||
const CommandRunner = require('./CommandRunner')
|
||||
const { emitPdfStats } = require('./ContentCacheMetrics')
|
||||
const SynctexOutputParser = require('./SynctexOutputParser')
|
||||
const {
|
||||
downloadLatestCompileCache,
|
||||
downloadOutputDotSynctexFromCompileCache,
|
||||
} = require('./CLSICacheHandler')
|
||||
const StatsManager = require('./StatsManager')
|
||||
const SafeReader = require('./SafeReader')
|
||||
const { enableLatexMkMetrics, addLatexFdbMetrics } = require('./LatexMetrics')
|
||||
const { callbackifyMultiResult } = require('@overleaf/promise-utils')
|
||||
const { shouldSkipMetrics } = require('./Metrics')
|
||||
const { downloadLatestCompileCache, downloadOutputDotSynctexFromCompileCache } =
|
||||
CLSICacheHandler
|
||||
const { emitPdfStats } = ContentCacheMetrics
|
||||
const { enableLatexMkMetrics, addLatexFdbMetrics } = LatexMetrics
|
||||
const { shouldSkipMetrics } = ClsiMetrics
|
||||
|
||||
const KNOWN_LATEXMK_RULES = new Set([
|
||||
'biber',
|
||||
@@ -848,7 +848,7 @@ function _emitMetrics(request, status, stats, timings) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
doCompileWithLock: callbackify(doCompileWithLock),
|
||||
stopCompile: callbackify(stopCompile),
|
||||
clearProject: callbackify(clearProject),
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
* ContentCacheManager - maintains a cache of stream hashes from a PDF file
|
||||
*/
|
||||
|
||||
const { callbackify } = require('node:util')
|
||||
const fs = require('node:fs')
|
||||
const crypto = require('node:crypto')
|
||||
const Path = require('node:path')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const pLimit = require('p-limit')
|
||||
const { parseXrefTable } = require('./XrefParser')
|
||||
const {
|
||||
import { callbackify } from 'node:util'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import Path from 'node:path'
|
||||
import Settings from '@overleaf/settings'
|
||||
import OError from '@overleaf/o-error'
|
||||
import pLimit from 'p-limit'
|
||||
import XrefParser from './XrefParser.js'
|
||||
import {
|
||||
QueueLimitReachedError,
|
||||
TimedOutError,
|
||||
NoXrefTableError,
|
||||
} = require('./Errors')
|
||||
const workerpool = require('workerpool')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
} from './Errors.js'
|
||||
import workerpool from 'workerpool'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
|
||||
const { parseXrefTable } = XrefParser
|
||||
|
||||
/**
|
||||
* @type {import('workerpool').WorkerPool}
|
||||
@@ -24,14 +27,17 @@ const Metrics = require('@overleaf/metrics')
|
||||
let WORKER_POOL
|
||||
// NOTE: Check for main thread to avoid recursive start of pool.
|
||||
if (Settings.pdfCachingEnableWorkerPool && workerpool.isMainThread) {
|
||||
WORKER_POOL = workerpool.pool(Path.join(__dirname, 'ContentCacheWorker.js'), {
|
||||
// Cap number of worker threads.
|
||||
maxWorkers: Settings.pdfCachingWorkerPoolSize,
|
||||
// Warmup workers.
|
||||
minWorkers: Settings.pdfCachingWorkerPoolSize,
|
||||
// Limit queue back-log
|
||||
maxQueueSize: Settings.pdfCachingWorkerPoolBackLogLimit,
|
||||
})
|
||||
WORKER_POOL = workerpool.pool(
|
||||
Path.join(import.meta.dirname, 'ContentCacheWorker.js'),
|
||||
{
|
||||
// Cap number of worker threads.
|
||||
maxWorkers: Settings.pdfCachingWorkerPoolSize,
|
||||
// Warmup workers.
|
||||
minWorkers: Settings.pdfCachingWorkerPoolSize,
|
||||
// Limit queue back-log
|
||||
maxQueueSize: Settings.pdfCachingWorkerPoolBackLogLimit,
|
||||
}
|
||||
)
|
||||
setInterval(() => {
|
||||
const {
|
||||
totalWorkers,
|
||||
@@ -431,7 +437,7 @@ function promiseMapWithLimit(concurrency, array, fn) {
|
||||
return Promise.all(array.map(x => limit(() => fn(x))))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
HASH_REGEX: /^[0-9a-f]{64}$/,
|
||||
update: callbackify(update),
|
||||
promises: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const logger = require('@overleaf/logger')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const os = require('node:os')
|
||||
import logger from '@overleaf/logger'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import os from 'node:os'
|
||||
|
||||
let CACHED_LOAD = {
|
||||
expires: -1,
|
||||
@@ -141,6 +141,6 @@ function emitPdfCachingStats(stats, timings, request) {
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
emitPdfStats,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const workerpool = require('workerpool')
|
||||
const ContentCacheManager = require('./ContentCacheManager')
|
||||
import workerpool from 'workerpool'
|
||||
import ContentCacheManager from './ContentCacheManager.js'
|
||||
|
||||
workerpool.worker(ContentCacheManager.promises)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Path = require('node:path')
|
||||
const send = require('send')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const OutputCacheManager = require('./OutputCacheManager')
|
||||
import Path from 'node:path'
|
||||
import send from 'send'
|
||||
import Settings from '@overleaf/settings'
|
||||
import OutputCacheManager from './OutputCacheManager.js'
|
||||
|
||||
const ONE_DAY_S = 24 * 60 * 60
|
||||
const ONE_DAY_MS = ONE_DAY_S * 1000
|
||||
@@ -21,4 +21,4 @@ function getPdfRange(req, res, next) {
|
||||
send(req, path).pipe(res)
|
||||
}
|
||||
|
||||
module.exports = { getPdfRange }
|
||||
export default { getPdfRange }
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
import Path from 'node:path'
|
||||
let ContentTypeMapper
|
||||
const Path = require('node:path')
|
||||
|
||||
// here we coerce html, css and js to text/plain,
|
||||
// otherwise choose correct mime type based on file extension,
|
||||
// falling back to octet-stream
|
||||
module.exports = ContentTypeMapper = {
|
||||
export default ContentTypeMapper = {
|
||||
map(path) {
|
||||
switch (Path.extname(path)) {
|
||||
case '.txt':
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import logger from '@overleaf/logger'
|
||||
let LockManager
|
||||
const logger = require('@overleaf/logger')
|
||||
|
||||
const LockState = {} // locks for docker container operations, by container name
|
||||
|
||||
module.exports = LockManager = {
|
||||
export default LockManager = {
|
||||
MAX_LOCK_HOLD_TIME: 15000, // how long we can keep a lock
|
||||
MAX_LOCK_WAIT_TIME: 10000, // how long we wait for a lock
|
||||
LOCK_TEST_INTERVAL: 1000, // retry time
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const fsPromises = require('node:fs/promises')
|
||||
const { callbackify } = require('node:util')
|
||||
const logger = require('@overleaf/logger')
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import { callbackify } from 'node:util'
|
||||
import logger from '@overleaf/logger'
|
||||
|
||||
async function injectDraftMode(filename) {
|
||||
const content = await fsPromises.readFile(filename, { encoding: 'utf8' })
|
||||
@@ -18,7 +18,7 @@ async function injectDraftMode(filename) {
|
||||
await fsPromises.writeFile(filename, modifiedContent, { encoding: 'utf8' })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
injectDraftMode: callbackify(injectDraftMode),
|
||||
promises: { injectDraftMode },
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
/* eslint-disable
|
||||
no-proto,
|
||||
no-unused-vars,
|
||||
*/
|
||||
/* eslint-disable no-proto
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
const OError = require('@overleaf/o-error')
|
||||
import OError from '@overleaf/o-error'
|
||||
|
||||
let Errors
|
||||
function NotFoundError(message) {
|
||||
export function NotFoundError(message) {
|
||||
const error = new Error(message)
|
||||
error.name = 'NotFoundError'
|
||||
error.__proto__ = NotFoundError.prototype
|
||||
@@ -15,7 +12,7 @@ function NotFoundError(message) {
|
||||
}
|
||||
NotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
function FilesOutOfSyncError(message) {
|
||||
export function FilesOutOfSyncError(message) {
|
||||
const error = new Error(message)
|
||||
error.name = 'FilesOutOfSyncError'
|
||||
error.__proto__ = FilesOutOfSyncError.prototype
|
||||
@@ -23,7 +20,7 @@ function FilesOutOfSyncError(message) {
|
||||
}
|
||||
FilesOutOfSyncError.prototype.__proto__ = Error.prototype
|
||||
|
||||
function AlreadyCompilingError(message) {
|
||||
export function AlreadyCompilingError(message) {
|
||||
const error = new Error(message)
|
||||
error.name = 'AlreadyCompilingError'
|
||||
error.__proto__ = AlreadyCompilingError.prototype
|
||||
@@ -31,13 +28,13 @@ function AlreadyCompilingError(message) {
|
||||
}
|
||||
AlreadyCompilingError.prototype.__proto__ = Error.prototype
|
||||
|
||||
class QueueLimitReachedError extends OError {}
|
||||
class TimedOutError extends OError {}
|
||||
class NoXrefTableError extends OError {}
|
||||
class TooManyCompileRequestsError extends OError {}
|
||||
class InvalidParameter extends OError {}
|
||||
export class QueueLimitReachedError extends OError {}
|
||||
export class TimedOutError extends OError {}
|
||||
export class NoXrefTableError extends OError {}
|
||||
export class TooManyCompileRequestsError extends OError {}
|
||||
export class InvalidParameter extends OError {}
|
||||
|
||||
module.exports = Errors = {
|
||||
export default {
|
||||
QueueLimitReachedError,
|
||||
TimedOutError,
|
||||
NotFoundError,
|
||||
|
||||
@@ -339,4 +339,4 @@ function convertToArray(object) {
|
||||
.sort((a, b) => b.size - a.size) // sort by size descending
|
||||
}
|
||||
|
||||
module.exports = { enableLatexMkMetrics, addLatexMkMetrics, addLatexFdbMetrics }
|
||||
export default { enableLatexMkMetrics, addLatexMkMetrics, addLatexFdbMetrics }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const Path = require('node:path')
|
||||
const { promisify } = require('node:util')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
const CommandRunner = require('./CommandRunner')
|
||||
const { addLatexMkMetrics } = require('./LatexMetrics')
|
||||
const fs = require('node:fs')
|
||||
import Path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import Settings from '@overleaf/settings'
|
||||
import logger from '@overleaf/logger'
|
||||
import CommandRunner from './CommandRunner.js'
|
||||
import LatexMetrics from './LatexMetrics.js'
|
||||
import fs from 'node:fs'
|
||||
|
||||
const { addLatexMkMetrics } = LatexMetrics
|
||||
|
||||
const ProcessTable = {} // table of currently running jobs (pids or docker container names)
|
||||
|
||||
@@ -203,7 +205,7 @@ function _buildLatexCommand(mainFile, opts = {}) {
|
||||
return command
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
runLatex,
|
||||
killLatex,
|
||||
promises: {
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import { spawn } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import _ from 'lodash'
|
||||
import logger from '@overleaf/logger'
|
||||
let CommandRunner
|
||||
const { spawn } = require('node:child_process')
|
||||
const { promisify } = require('node:util')
|
||||
const _ = require('lodash')
|
||||
const logger = require('@overleaf/logger')
|
||||
|
||||
logger.debug('using standard command runner')
|
||||
|
||||
module.exports = CommandRunner = {
|
||||
export default CommandRunner = {
|
||||
run(
|
||||
projectId,
|
||||
command,
|
||||
@@ -106,7 +106,7 @@ module.exports = CommandRunner = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.promises = {
|
||||
CommandRunner.promises = {
|
||||
run: promisify(CommandRunner.run),
|
||||
kill: promisify(CommandRunner.kill),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const logger = require('@overleaf/logger')
|
||||
const Errors = require('./Errors')
|
||||
const RequestParser = require('./RequestParser')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import logger from '@overleaf/logger'
|
||||
import Errors from './Errors.js'
|
||||
import RequestParser from './RequestParser.js'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
// The lock timeout should be higher than the maximum end-to-end compile time.
|
||||
// Here, we use the maximum compile timeout plus 2 minutes.
|
||||
@@ -63,4 +63,4 @@ class Lock {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { acquire }
|
||||
export default { acquire }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Path = require('node:path')
|
||||
import Path from 'node:path'
|
||||
|
||||
const CLSI_REQUEST_SERIALIZED_PROPERTIES = [
|
||||
'compiler',
|
||||
@@ -16,7 +16,7 @@ const CLSI_REQUEST_SERIALIZED_PROPERTIES = [
|
||||
'syncType',
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
/**
|
||||
* Serializer for a CLSI request object.
|
||||
* Only includes properties useful for logging.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { prom } = require('@overleaf/metrics')
|
||||
import { prom } from '@overleaf/metrics'
|
||||
|
||||
const COMPILE_TIME_BUCKETS = [
|
||||
0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 25, 30, 45, 60, 75, 90, 120, 150,
|
||||
@@ -74,7 +74,7 @@ function shouldSkipMetrics(request) {
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
compilesTotal,
|
||||
compileDurationSeconds,
|
||||
e2eCompileDurationSeconds,
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
let OutputCacheManager
|
||||
const { callbackify, promisify } = require('node:util')
|
||||
const async = require('async')
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const logger = require('@overleaf/logger')
|
||||
const _ = require('lodash')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const crypto = require('node:crypto')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
|
||||
const OutputFileOptimiser = require('./OutputFileOptimiser')
|
||||
const ContentCacheManager = require('./ContentCacheManager')
|
||||
const {
|
||||
import { callbackify, promisify } from 'node:util'
|
||||
import async from 'async'
|
||||
import fs from 'node:fs'
|
||||
import Path from 'node:path'
|
||||
import logger from '@overleaf/logger'
|
||||
import _ from 'lodash'
|
||||
import Settings from '@overleaf/settings'
|
||||
import crypto from 'node:crypto'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import OutputFileOptimiser from './OutputFileOptimiser.js'
|
||||
import ContentCacheManager from './ContentCacheManager.js'
|
||||
import {
|
||||
QueueLimitReachedError,
|
||||
TimedOutError,
|
||||
NoXrefTableError,
|
||||
} = require('./Errors')
|
||||
} from './Errors.js'
|
||||
let OutputCacheManager
|
||||
|
||||
const OLDEST_BUILD_DIR = new Map()
|
||||
const PENDING_PROJECT_ACTIONS = new Map()
|
||||
@@ -101,7 +100,7 @@ async function queueDirOperation(dir, fn) {
|
||||
return p
|
||||
}
|
||||
|
||||
module.exports = OutputCacheManager = {
|
||||
export default OutputCacheManager = {
|
||||
CONTENT_SUBDIR: 'content',
|
||||
CACHE_SUBDIR: 'generated-files',
|
||||
ARCHIVE_SUBDIR: 'archived-logs',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const OutputFileArchiveManager = require('./OutputFileArchiveManager')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const { pipeline } = require('node:stream/promises')
|
||||
import OutputFileArchiveManager from './OutputFileArchiveManager.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
|
||||
async function createOutputZip(req, res) {
|
||||
const {
|
||||
@@ -20,4 +20,4 @@ async function createOutputZip(req, res) {
|
||||
await pipeline(archive, res)
|
||||
}
|
||||
|
||||
module.exports = { createOutputZip: expressify(createOutputZip) }
|
||||
export default { createOutputZip: expressify(createOutputZip) }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const archiver = require('archiver')
|
||||
const OutputCacheManager = require('./OutputCacheManager')
|
||||
const OutputFileFinder = require('./OutputFileFinder')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const { open } = require('node:fs/promises')
|
||||
const { NotFoundError } = require('./Errors')
|
||||
const logger = require('@overleaf/logger')
|
||||
import archiver from 'archiver'
|
||||
import OutputCacheManager from './OutputCacheManager.js'
|
||||
import OutputFileFinder from './OutputFileFinder.js'
|
||||
import Settings from '@overleaf/settings'
|
||||
import { open } from 'node:fs/promises'
|
||||
import { NotFoundError } from './Errors.js'
|
||||
import logger from '@overleaf/logger'
|
||||
|
||||
// NOTE: Updating this list requires a corresponding change in
|
||||
// * services/web/frontend/js/features/pdf-preview/util/file-list.ts
|
||||
@@ -20,7 +20,7 @@ function getContentDir(projectId, userId) {
|
||||
return `${Settings.path.outputDir}/${subDir}/`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
async archiveFilesForBuild(projectId, userId, build) {
|
||||
logger.debug({ projectId, userId, build }, 'Will create zip file')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const { callbackifyMultiResult } = require('@overleaf/promise-utils')
|
||||
import Path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { callbackifyMultiResult } from '@overleaf/promise-utils'
|
||||
|
||||
async function walkFolder(compileDir, d, files, allEntries) {
|
||||
const dirents = await fs.promises.readdir(Path.join(compileDir, d), {
|
||||
@@ -42,7 +42,7 @@ async function findOutputFiles(resources, directory) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
findOutputFiles: callbackifyMultiResult(findOutputFiles, [
|
||||
'outputFiles',
|
||||
'allEntries',
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import Path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import logger from '@overleaf/logger'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import _ from 'lodash'
|
||||
let OutputFileOptimiser
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const { spawn } = require('node:child_process')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const _ = require('lodash')
|
||||
|
||||
module.exports = OutputFileOptimiser = {
|
||||
export default OutputFileOptimiser = {
|
||||
optimiseFile(src, dst, callback) {
|
||||
// check output file (src) and see if we can optimise it, storing
|
||||
// the result in the build directory (dst)
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import UrlCache from './UrlCache.js'
|
||||
import CompileManager from './CompileManager.js'
|
||||
import async from 'async'
|
||||
import logger from '@overleaf/logger'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import Settings from '@overleaf/settings'
|
||||
import { callbackify } from 'node:util'
|
||||
import Path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
let ProjectPersistenceManager
|
||||
const UrlCache = require('./UrlCache')
|
||||
const CompileManager = require('./CompileManager')
|
||||
const async = require('async')
|
||||
const logger = require('@overleaf/logger')
|
||||
const oneDay = 24 * 60 * 60 * 1000
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const { callbackify } = require('node:util')
|
||||
const Path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
|
||||
// projectId -> timestamp mapping.
|
||||
const LAST_ACCESS = new Map()
|
||||
@@ -87,7 +87,7 @@ async function refreshExpiryTimeout() {
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = ProjectPersistenceManager = {
|
||||
export default ProjectPersistenceManager = {
|
||||
EXPIRY_TIMEOUT: Settings.project_cache_length_ms || oneDay * 2.5,
|
||||
|
||||
isAnyDiskLow() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const settings = require('@overleaf/settings')
|
||||
const OutputCacheManager = require('./OutputCacheManager')
|
||||
import settings from '@overleaf/settings'
|
||||
import OutputCacheManager from './OutputCacheManager.js'
|
||||
|
||||
const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
|
||||
const MAX_TIMEOUT = 600
|
||||
@@ -247,4 +247,4 @@ function _checkPath(path) {
|
||||
return path
|
||||
}
|
||||
|
||||
module.exports = { parse, MAX_TIMEOUT }
|
||||
export default { parse, MAX_TIMEOUT }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const Path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Errors = require('./Errors')
|
||||
const SafeReader = require('./SafeReader')
|
||||
import Path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import logger from '@overleaf/logger'
|
||||
import Errors from './Errors.js'
|
||||
import SafeReader from './SafeReader.js'
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
// The sync state is an identifier which must match for an
|
||||
// incremental update to be allowed.
|
||||
//
|
||||
|
||||
@@ -12,22 +12,25 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import { promisify } from 'node:util'
|
||||
import UrlCache from './UrlCache.js'
|
||||
import Path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import async from 'async'
|
||||
import OutputFileFinder from './OutputFileFinder.js'
|
||||
import ResourceStateManager from './ResourceStateManager.js'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import logger from '@overleaf/logger'
|
||||
import settings from '@overleaf/settings'
|
||||
import ClsiMetrics from './Metrics.js'
|
||||
|
||||
const { shouldSkipMetrics } = ClsiMetrics
|
||||
|
||||
let ResourceWriter
|
||||
const { promisify } = require('node:util')
|
||||
const UrlCache = require('./UrlCache')
|
||||
const Path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const async = require('async')
|
||||
const OutputFileFinder = require('./OutputFileFinder')
|
||||
const ResourceStateManager = require('./ResourceStateManager')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const logger = require('@overleaf/logger')
|
||||
const settings = require('@overleaf/settings')
|
||||
const { shouldSkipMetrics } = require('./Metrics')
|
||||
|
||||
const parallelFileDownloads = settings.parallelFileDownloads || 1
|
||||
|
||||
module.exports = ResourceWriter = {
|
||||
export default ResourceWriter = {
|
||||
syncResourcesToDisk(request, basePath, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
@@ -375,7 +378,7 @@ module.exports = ResourceWriter = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.promises = {
|
||||
ResourceWriter.promises = {
|
||||
syncResourcesToDisk: promisify(ResourceWriter.syncResourcesToDisk),
|
||||
saveIncrementalResourcesToDisk: promisify(
|
||||
ResourceWriter.saveIncrementalResourcesToDisk
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import logger from '@overleaf/logger'
|
||||
import { promisifyMultiResult } from '@overleaf/promise-utils'
|
||||
let SafeReader
|
||||
const fs = require('node:fs')
|
||||
const logger = require('@overleaf/logger')
|
||||
const { promisifyMultiResult } = require('@overleaf/promise-utils')
|
||||
|
||||
module.exports = SafeReader = {
|
||||
export default SafeReader = {
|
||||
// safely read up to size bytes from a file and return result as a
|
||||
// string
|
||||
|
||||
@@ -62,6 +62,6 @@ module.exports = SafeReader = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.promises = {
|
||||
SafeReader.promises = {
|
||||
readFile: promisifyMultiResult(SafeReader.readFile, ['result', 'bytesRead']),
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import Path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import Settings from '@overleaf/settings'
|
||||
import logger from '@overleaf/logger'
|
||||
let ForbidSymlinks
|
||||
const Path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
|
||||
module.exports = ForbidSymlinks = function (staticFn, root, options) {
|
||||
export default ForbidSymlinks = function (staticFn, root, options) {
|
||||
const expressStatic = staticFn(root, options)
|
||||
const basePath = Path.resolve(root)
|
||||
return function (req, res, next) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const crypto = require('node:crypto')
|
||||
const { shouldSkipMetrics } = require('./Metrics')
|
||||
import crypto from 'node:crypto'
|
||||
import Metrics from './Metrics.js'
|
||||
|
||||
const { shouldSkipMetrics } = Metrics
|
||||
|
||||
/**
|
||||
* Consistently sample a keyspace with a given sample percentage.
|
||||
@@ -46,4 +48,4 @@ function sampleRequest(request, samplingPercentage) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { sampleByHash, sampleRequest }
|
||||
export default { sampleByHash, sampleRequest }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Path = require('node:path')
|
||||
import Path from 'node:path'
|
||||
|
||||
/**
|
||||
* Parse output from the `synctex view` command
|
||||
@@ -110,4 +110,4 @@ function _setFloatProp(record, prop, value) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { parseViewOutput, parseEditOutput }
|
||||
export default { parseViewOutput, parseEditOutput }
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import Path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import ResourceWriter from './ResourceWriter.js'
|
||||
import SafeReader from './SafeReader.js'
|
||||
import logger from '@overleaf/logger'
|
||||
let TikzManager
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const { promisify } = require('node:util')
|
||||
const ResourceWriter = require('./ResourceWriter')
|
||||
const SafeReader = require('./SafeReader')
|
||||
const logger = require('@overleaf/logger')
|
||||
|
||||
// for \tikzexternalize or pstool to work the main file needs to match the
|
||||
// jobname. Since we set the -jobname to output, we have to create a
|
||||
// copy of the main file as 'output.tex'.
|
||||
|
||||
module.exports = TikzManager = {
|
||||
export default TikzManager = {
|
||||
checkMainFile(compileDir, mainFile, resources, callback) {
|
||||
// if there's already an output.tex file, we don't want to touch it
|
||||
if (callback == null) {
|
||||
@@ -103,7 +103,7 @@ module.exports = TikzManager = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.promises = {
|
||||
TikzManager.promises = {
|
||||
checkMainFile: promisify(TikzManager.checkMainFile),
|
||||
injectOutputFile: promisify(TikzManager.injectOutputFile),
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const UrlFetcher = require('./UrlFetcher')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const { callbackify } = require('node:util')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
import UrlFetcher from './UrlFetcher.js'
|
||||
|
||||
import Settings from '@overleaf/settings'
|
||||
import fs from 'node:fs'
|
||||
import Path from 'node:path'
|
||||
import { callbackify } from 'node:util'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
|
||||
const PENDING_DOWNLOADS = new Map()
|
||||
|
||||
@@ -120,7 +121,7 @@ async function download(url, fallbackURL, cachePath) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
clearProject: callbackify(clearProject),
|
||||
createProjectDir: callbackify(createProjectDir),
|
||||
downloadUrlToFile: callbackify(downloadUrlToFile),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
const fs = require('node:fs')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const {
|
||||
import fs from 'node:fs'
|
||||
import logger from '@overleaf/logger'
|
||||
import Settings from '@overleaf/settings'
|
||||
import {
|
||||
CustomHttpAgent,
|
||||
CustomHttpsAgent,
|
||||
fetchStream,
|
||||
RequestFailedError,
|
||||
} = require('@overleaf/fetch-utils')
|
||||
const { URL } = require('node:url')
|
||||
const { pipeline } = require('node:stream/promises')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
} from '@overleaf/fetch-utils'
|
||||
import { URL } from 'node:url'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
|
||||
const MAX_CONNECT_TIME = 1000
|
||||
const httpAgent = new CustomHttpAgent({ connectTimeout: MAX_CONNECT_TIME })
|
||||
@@ -121,6 +121,8 @@ function inferSource(url) {
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
module.exports.promises = {
|
||||
pipeUrlToFileWithRetry,
|
||||
export default {
|
||||
promises: {
|
||||
pipeUrlToFileWithRetry,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { NoXrefTableError } = require('./Errors')
|
||||
const fs = require('node:fs')
|
||||
import { NoXrefTableError } from './Errors.js'
|
||||
import fs from 'node:fs'
|
||||
const { O_RDONLY, O_NOFOLLOW } = fs.constants
|
||||
const MAX_XREF_FILE_SIZE = 1024 * 1024
|
||||
|
||||
@@ -62,6 +62,6 @@ async function parseXrefTable(filePath, pdfFileSize) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
parseXrefTable,
|
||||
}
|
||||
|
||||
@@ -7,4 +7,6 @@ clsi
|
||||
--node-version=22.18.0
|
||||
--pipeline-owner=🚉 Platform
|
||||
--public-repo=True
|
||||
--test-unit-vitest=True
|
||||
--tsconfig-extra-includes=vitest.config.unit.cjs
|
||||
--use-large-ci-runner=True
|
||||
|
||||
+10
@@ -1,5 +1,6 @@
|
||||
const Path = require('node:path')
|
||||
const os = require('node:os')
|
||||
const fs = require('node:fs')
|
||||
|
||||
const isPreEmptible = process.env.PREEMPTIBLE === 'TRUE'
|
||||
const CLSI_SERVER_ID = os.hostname().replace('-ctr', '')
|
||||
@@ -96,6 +97,15 @@ if (process.env.ALLOWED_COMPILE_GROUPS) {
|
||||
}
|
||||
|
||||
if ((process.env.DOCKER_RUNNER || process.env.SANDBOXED_COMPILES) === 'true') {
|
||||
if (
|
||||
!fs.existsSync(Path.join(__dirname, '..', 'app', 'js', 'DockerRunner.js'))
|
||||
) {
|
||||
console.error(
|
||||
'Sandboxed compiles are only available with Overleaf Server Pro. Compare Server Pro with Community Edition here: https://docs.overleaf.com/on-premises/welcome/server-pro-vs.-community-edition'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
module.exports.clsi = {
|
||||
dockerRunner: true,
|
||||
docker: {
|
||||
@@ -7,12 +7,14 @@ services:
|
||||
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
|
||||
volumes:
|
||||
- ./reports:/overleaf/services/clsi/reports
|
||||
- ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
|
||||
command: npm run test:unit:_run
|
||||
environment:
|
||||
CI:
|
||||
MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
|
||||
NODE_ENV: test
|
||||
NODE_OPTIONS: "--unhandled-rejections=strict"
|
||||
VITEST_NO_CACHE: true
|
||||
|
||||
|
||||
test_acceptance:
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
- .:/overleaf/services/clsi
|
||||
- ../../node_modules:/overleaf/node_modules
|
||||
- ../../libraries:/overleaf/libraries
|
||||
- ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
|
||||
working_dir: /overleaf/services/clsi
|
||||
environment:
|
||||
MOCHA_GREP: ${MOCHA_GREP}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
"description": "A Node.js implementation of the CLSI LaTeX web-API",
|
||||
"private": true,
|
||||
"main": "app.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test:acceptance:_run": "mocha --recursive --timeout 15000 --exit $@ test/acceptance/js",
|
||||
"test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
|
||||
"test:unit:_run": "mocha --recursive --exit $@ test/unit/js",
|
||||
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
|
||||
"test:unit:_run": "vitest --config ./vitest.config.unit.cjs",
|
||||
"test:unit": "npm run test:unit:_run",
|
||||
"nodemon": "node --watch app.js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts}'",
|
||||
@@ -45,10 +46,10 @@
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"mock-fs": "^5.1.2",
|
||||
"node-fetch": "^2.7.0",
|
||||
"sandboxed-module": "^2.0.4",
|
||||
"sinon": "~9.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"timekeeper": "2.2.0",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const fs = require('node:fs')
|
||||
const { parseXrefTable } = require('../app/lib/pdfjs/parseXrefTable')
|
||||
import fs from 'node:fs'
|
||||
import XrefParser from '../app/js/XrefParser.js'
|
||||
|
||||
const pdfPath = process.argv[2]
|
||||
|
||||
async function main() {
|
||||
const size = (await fs.promises.stat(pdfPath)).size
|
||||
const { xRefEntries } = await parseXrefTable(pdfPath, size)
|
||||
const { xRefEntries } = await XrefParser.parseXrefTable(pdfPath, size)
|
||||
console.log('Xref entries', xRefEntries)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
import Client from './helpers/Client.js'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('AllowedImageNames', function () {
|
||||
beforeEach(async function () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
import Client from './helpers/Client.js'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Broken LaTeX file', function () {
|
||||
before(async function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
import Client from './helpers/Client.js'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
|
||||
describe('Deleting Old Files', function () {
|
||||
before(async function () {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const fetch = require('node-fetch')
|
||||
const Stream = require('node:stream')
|
||||
const fs = require('node:fs')
|
||||
const fsPromises = require('node:fs/promises')
|
||||
const ChildProcess = require('node:child_process')
|
||||
const { promisify } = require('node:util')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const Path = require('node:path')
|
||||
import Client from './helpers/Client.js'
|
||||
import fetch from 'node-fetch'
|
||||
import Stream from 'node:stream'
|
||||
import fs from 'node:fs'
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import ChildProcess from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import Path from 'node:path'
|
||||
import process from 'node:process'
|
||||
const fixturePath = path => {
|
||||
if (path.slice(0, 3) === 'tmp') {
|
||||
return '/tmp/clsi_acceptance_tests' + path.slice(3)
|
||||
}
|
||||
return Path.join(__dirname, '../fixtures/', path)
|
||||
return Path.join(import.meta.dirname, '../fixtures/', path)
|
||||
}
|
||||
const process = require('node:process')
|
||||
const pipeline = promisify(Stream.pipeline)
|
||||
console.log(
|
||||
process.pid,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const { fetchNothing, fetchString } = require('@overleaf/fetch-utils')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import Client from './helpers/Client.js'
|
||||
import { fetchNothing, fetchString } from '@overleaf/fetch-utils'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import { expect } from 'chai'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
describe('Simple LaTeX file', function () {
|
||||
const content = `\
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
import Client from './helpers/Client.js'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Smoke Test', function () {
|
||||
before(async function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { fetchString } = require('@overleaf/fetch-utils')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import { fetchString } from '@overleaf/fetch-utils'
|
||||
import Settings from '@overleaf/settings'
|
||||
after(async function () {
|
||||
const metrics = await fetchString(`${Settings.apis.clsi.url}/metrics`)
|
||||
console.error('-- metrics --')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { promisify } = require('node:util')
|
||||
const Client = require('./helpers/Client')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
import { promisify } from 'node:util'
|
||||
import Client from './helpers/Client.js'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
const sleep = promisify(setTimeout)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const { expect } = require('chai')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
import Client from './helpers/Client.js'
|
||||
import { expect } from 'chai'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
|
||||
describe('Syncing', function () {
|
||||
before(async function () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { expect } = require('chai')
|
||||
import Client from './helpers/Client.js'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Timed out compile', function () {
|
||||
before(async function () {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
const express = require('express')
|
||||
const Path = require('node:path')
|
||||
const Client = require('./helpers/Client')
|
||||
const sinon = require('sinon')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
const { fetchString } = require('@overleaf/fetch-utils')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import express from 'express'
|
||||
import Path from 'node:path'
|
||||
import Client from './helpers/Client.js'
|
||||
import sinon from 'sinon'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
import { fetchString } from '@overleaf/fetch-utils'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
const Server = {
|
||||
run() {
|
||||
const app = express()
|
||||
|
||||
const staticServer = express.static(Path.join(__dirname, '../fixtures/'))
|
||||
const staticServer = express.static(
|
||||
Path.join(import.meta.dirname, '../fixtures/')
|
||||
)
|
||||
|
||||
const alreadyFailed = new Map()
|
||||
app.get('/fail/:times/:id', (req, res) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const Client = require('./helpers/Client')
|
||||
const { expect } = require('chai')
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const ClsiApp = require('./helpers/ClsiApp')
|
||||
import Client from './helpers/Client.js'
|
||||
import { expect } from 'chai'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import ClsiApp from './helpers/ClsiApp.js'
|
||||
|
||||
describe('Syncing', function () {
|
||||
before(async function () {
|
||||
@@ -11,7 +11,7 @@ describe('Syncing', function () {
|
||||
{
|
||||
path: 'main.tex',
|
||||
content: fs.readFileSync(
|
||||
path.join(__dirname, '../fixtures/naugty_strings.txt'),
|
||||
path.join(import.meta.dirname, '../fixtures/naugty_strings.txt'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
const express = require('express')
|
||||
const {
|
||||
fetchJson,
|
||||
fetchNothing,
|
||||
fetchString,
|
||||
} = require('@overleaf/fetch-utils')
|
||||
const fs = require('node:fs')
|
||||
const fsPromises = require('node:fs/promises')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import express from 'express'
|
||||
import { fetchJson, fetchNothing, fetchString } from '@overleaf/fetch-utils'
|
||||
import fs from 'node:fs'
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
const host = Settings.apis.clsi.url
|
||||
|
||||
@@ -187,7 +183,7 @@ function smokeTest() {
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
randomId,
|
||||
compile,
|
||||
stopCompile,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const app = require('../../../../app')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import app from '../../../../app.js'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
function startApp() {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -26,6 +26,6 @@ async function ensureRunning() {
|
||||
await appStartedPromise
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
ensureRunning,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const ContentCacheManager = require('../../app/js/ContentCacheManager')
|
||||
const fs = require('node:fs')
|
||||
const crypto = require('node:crypto')
|
||||
const path = require('node:path')
|
||||
const os = require('node:os')
|
||||
const async = require('async')
|
||||
import ContentCacheManager from '../../app/js/ContentCacheManager.js'
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
import async from 'async'
|
||||
const _createHash = crypto.createHash
|
||||
|
||||
const files = process.argv.slice(2)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const { fetchNothing } = require('@overleaf/fetch-utils')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const async = require('async')
|
||||
const fs = require('node:fs')
|
||||
const _ = require('lodash')
|
||||
import { fetchNothing } from '@overleaf/fetch-utils'
|
||||
import Settings from '@overleaf/settings'
|
||||
import async from 'async'
|
||||
import fs from 'node:fs'
|
||||
import _ from 'lodash'
|
||||
const concurentCompiles = 5
|
||||
const totalCompiles = 50
|
||||
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
const chai = require('chai')
|
||||
const sinonChai = require('sinon-chai')
|
||||
const chaiAsPromised = require('chai-as-promised')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
import chai from 'chai'
|
||||
import sinonChai from 'sinon-chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
|
||||
// Setup chai
|
||||
chai.should()
|
||||
chai.use(sinonChai)
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
// Global SandboxedModule settings
|
||||
SandboxedModule.configure({
|
||||
requires: {
|
||||
'@overleaf/logger': {
|
||||
debug() {},
|
||||
log() {},
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
err() {},
|
||||
},
|
||||
},
|
||||
globals: { Buffer, console, process, URL, Math },
|
||||
sourceTransformers: {
|
||||
removeNodePrefix: function (source) {
|
||||
return source.replace(/require\(['"]node:/g, "require('")
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const request = require('request')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import request from 'request'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
const buildUrl = path =>
|
||||
`http://${Settings.internal.clsi.host}:${Settings.internal.clsi.port}/${path}`
|
||||
|
||||
const url = buildUrl(`project/smoketest-${process.pid}/compile`)
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
sendNewResult(res) {
|
||||
this._run(error => this._sendResponse(res, error))
|
||||
},
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
import { vi, describe, beforeEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import Errors from '../../../app/js/Errors.js'
|
||||
import path from 'node:path'
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/CompileController'
|
||||
)
|
||||
|
||||
describe('CompileController', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.buildId = 'build-id-123'
|
||||
|
||||
vi.doMock('../../../app/js/CompileManager', () => ({
|
||||
default: (ctx.CompileManager = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/RequestParser', () => ({
|
||||
default: (ctx.RequestParser = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: (ctx.Settings = {
|
||||
apis: {
|
||||
clsi: {
|
||||
url: 'http://clsi.example.com',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
downloadHost: 'http://localhost:3013',
|
||||
},
|
||||
clsiCache: {
|
||||
enabled: false,
|
||||
url: 'http://localhost:3044',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: {
|
||||
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/ProjectPersistenceManager', () => ({
|
||||
default: (ctx.ProjectPersistenceManager = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/CLSICacheHandler', () => ({
|
||||
default: {
|
||||
notifyCLSICacheAboutBuild: sinon.stub(),
|
||||
downloadLatestCompileCache: sinon.stub().resolves(),
|
||||
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/Errors', () => ({
|
||||
default: (ctx.Errors = Errors),
|
||||
}))
|
||||
|
||||
ctx.CompileController = (await import(modulePath)).default
|
||||
ctx.Settings.externalUrl = 'http://www.example.com'
|
||||
ctx.req = {}
|
||||
ctx.res = {}
|
||||
ctx.next = sinon.stub()
|
||||
})
|
||||
|
||||
describe('compile', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.req.body = {
|
||||
compile: 'mock-body',
|
||||
}
|
||||
ctx.req.params = { project_id: (ctx.project_id = 'project-id-123') }
|
||||
ctx.request = {
|
||||
compile: 'mock-parsed-request',
|
||||
}
|
||||
ctx.request_with_project_id = {
|
||||
compile: ctx.request.compile,
|
||||
project_id: ctx.project_id,
|
||||
}
|
||||
ctx.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
size: 1337,
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
ctx.RequestParser.parse = sinon.stub().callsArgWith(1, null, ctx.request)
|
||||
ctx.ProjectPersistenceManager.markProjectAsJustAccessed = sinon
|
||||
.stub()
|
||||
.callsArg(1)
|
||||
ctx.stats = { foo: 1 }
|
||||
ctx.timings = { bar: 2 }
|
||||
ctx.res.status = sinon.stub().returnsThis()
|
||||
ctx.res.send = sinon.stub()
|
||||
|
||||
ctx.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, ctx.stats)
|
||||
Object.assign(timings, ctx.timings)
|
||||
cb(null, {
|
||||
outputFiles: ctx.output_files,
|
||||
buildId: ctx.buildId,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('successfully', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should parse the request', ctx => {
|
||||
ctx.RequestParser.parse.calledWith(ctx.req.body).should.equal(true)
|
||||
})
|
||||
|
||||
it('should run the compile for the specified project', ctx => {
|
||||
ctx.CompileManager.doCompileWithLock
|
||||
.calledWith(ctx.request_with_project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should mark the project as accessed', ctx => {
|
||||
ctx.ProjectPersistenceManager.markProjectAsJustAccessed
|
||||
.calledWith(ctx.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the JSON response', ctx => {
|
||||
ctx.res.status.calledWith(200).should.equal(true)
|
||||
ctx.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'success',
|
||||
error: null,
|
||||
stats: ctx.stats,
|
||||
timings: ctx.timings,
|
||||
buildId: ctx.buildId,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a outputUrlPrefix', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.Settings.apis.clsi.outputUrlPrefix = ''
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with empty outputUrlPrefix', ctx => {
|
||||
ctx.res.status.calledWith(200).should.equal(true)
|
||||
ctx.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'success',
|
||||
error: null,
|
||||
stats: ctx.stats,
|
||||
timings: ctx.timings,
|
||||
buildId: ctx.buildId,
|
||||
outputUrlPrefix: '',
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with user provided fake_output.pdf', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.output_files = [
|
||||
{
|
||||
path: 'fake_output.pdf',
|
||||
type: 'pdf',
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
ctx.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, ctx.stats)
|
||||
Object.assign(timings, ctx.timings)
|
||||
cb(null, {
|
||||
outputFiles: ctx.output_files,
|
||||
buildId: ctx.buildId,
|
||||
})
|
||||
})
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with status failure', ctx => {
|
||||
ctx.res.status.calledWith(200).should.equal(true)
|
||||
ctx.res.send.should.have.been.calledWith({
|
||||
compile: {
|
||||
status: 'failure',
|
||||
error: null,
|
||||
stats: ctx.stats,
|
||||
timings: ctx.timings,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
buildId: ctx.buildId,
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an empty output.pdf', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
size: 0,
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
ctx.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, ctx.stats)
|
||||
Object.assign(timings, ctx.timings)
|
||||
cb(null, {
|
||||
outputFiles: ctx.output_files,
|
||||
buildId: ctx.buildId,
|
||||
})
|
||||
})
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with status failure', ctx => {
|
||||
ctx.res.status.calledWith(200).should.equal(true)
|
||||
ctx.res.send.should.have.been.calledWith({
|
||||
compile: {
|
||||
status: 'failure',
|
||||
error: null,
|
||||
stats: ctx.stats,
|
||||
buildId: ctx.buildId,
|
||||
timings: ctx.timings,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an error', () => {
|
||||
beforeEach(ctx => {
|
||||
const error = new Error((ctx.message = 'error message'))
|
||||
error.buildId = ctx.buildId
|
||||
ctx.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, ctx.stats)
|
||||
Object.assign(timings, ctx.timings)
|
||||
cb(error)
|
||||
})
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the error', ctx => {
|
||||
ctx.res.status.calledWith(500).should.equal(true)
|
||||
ctx.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'error',
|
||||
error: ctx.message,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
buildId: ctx.buildId,
|
||||
stats: ctx.stats,
|
||||
timings: ctx.timings,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with too many compile requests error', () => {
|
||||
beforeEach(ctx => {
|
||||
const error = new Errors.TooManyCompileRequestsError(
|
||||
'too many concurrent compile requests'
|
||||
)
|
||||
ctx.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, ctx.stats)
|
||||
Object.assign(timings, ctx.timings)
|
||||
cb(error)
|
||||
})
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the error', ctx => {
|
||||
ctx.res.status.calledWith(503).should.equal(true)
|
||||
ctx.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'unavailable',
|
||||
error: 'too many concurrent compile requests',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: ctx.stats,
|
||||
timings: ctx.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request times out', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.error = new Error((ctx.message = 'container timed out'))
|
||||
ctx.error.timedout = true
|
||||
ctx.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, ctx.stats)
|
||||
Object.assign(timings, ctx.timings)
|
||||
cb(ctx.error)
|
||||
})
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the timeout status', ctx => {
|
||||
ctx.res.status.calledWith(200).should.equal(true)
|
||||
ctx.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'timedout',
|
||||
error: ctx.message,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: ctx.stats,
|
||||
timings: ctx.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request returns no output files', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, ctx.stats)
|
||||
Object.assign(timings, ctx.timings)
|
||||
cb(null, {})
|
||||
})
|
||||
ctx.CompileController.compile(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the failure status', ctx => {
|
||||
ctx.res.status.calledWith(200).should.equal(true)
|
||||
ctx.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
error: null,
|
||||
status: 'failure',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: ctx.stats,
|
||||
timings: ctx.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromCode', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.file = 'main.tex'
|
||||
ctx.line = 42
|
||||
ctx.column = 5
|
||||
ctx.project_id = 'mock-project-id'
|
||||
ctx.req.params = { project_id: ctx.project_id }
|
||||
ctx.req.query = {
|
||||
file: ctx.file,
|
||||
line: ctx.line.toString(),
|
||||
column: ctx.column.toString(),
|
||||
}
|
||||
ctx.res.json = sinon.stub()
|
||||
|
||||
ctx.CompileManager.syncFromCode = sinon
|
||||
.stub()
|
||||
.yields(null, (ctx.pdfPositions = ['mock-positions']), true)
|
||||
ctx.CompileController.syncFromCode(ctx.req, ctx.res, ctx.next)
|
||||
})
|
||||
|
||||
it('should find the corresponding location in the PDF', ctx => {
|
||||
ctx.CompileManager.syncFromCode
|
||||
.calledWith(ctx.project_id, undefined, ctx.file, ctx.line, ctx.column)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the positions', ctx => {
|
||||
ctx.res.json
|
||||
.calledWith({
|
||||
pdf: ctx.pdfPositions,
|
||||
downloadedFromCache: true,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromPdf', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.page = 5
|
||||
ctx.h = 100.23
|
||||
ctx.v = 45.67
|
||||
ctx.project_id = 'mock-project-id'
|
||||
ctx.req.params = { project_id: ctx.project_id }
|
||||
ctx.req.query = {
|
||||
page: ctx.page.toString(),
|
||||
h: ctx.h.toString(),
|
||||
v: ctx.v.toString(),
|
||||
}
|
||||
ctx.res.json = sinon.stub()
|
||||
|
||||
ctx.CompileManager.syncFromPdf = sinon
|
||||
.stub()
|
||||
.yields(null, (ctx.codePositions = ['mock-positions']), true)
|
||||
ctx.CompileController.syncFromPdf(ctx.req, ctx.res, ctx.next)
|
||||
})
|
||||
|
||||
it('should find the corresponding location in the code', ctx => {
|
||||
ctx.CompileManager.syncFromPdf
|
||||
.calledWith(ctx.project_id, undefined, ctx.page, ctx.h, ctx.v)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the positions', ctx => {
|
||||
ctx.res.json
|
||||
.calledWith({
|
||||
code: ctx.codePositions,
|
||||
downloadedFromCache: true,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.file = 'main.tex'
|
||||
ctx.project_id = 'mock-project-id'
|
||||
ctx.req.params = { project_id: ctx.project_id }
|
||||
ctx.req.query = {
|
||||
file: ctx.file,
|
||||
image: (ctx.image = 'example.com/image'),
|
||||
}
|
||||
ctx.res.json = sinon.stub()
|
||||
|
||||
ctx.CompileManager.wordcount = sinon
|
||||
.stub()
|
||||
.callsArgWith(4, null, (ctx.texcount = ['mock-texcount']))
|
||||
})
|
||||
|
||||
it('should return the word count of a file', ctx => {
|
||||
ctx.CompileController.wordcount(ctx.req, ctx.res, ctx.next)
|
||||
ctx.CompileManager.wordcount
|
||||
.calledWith(ctx.project_id, undefined, ctx.file, ctx.image)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the texcount info', ctx => {
|
||||
ctx.CompileController.wordcount(ctx.req, ctx.res, ctx.next)
|
||||
ctx.res.json
|
||||
.calledWith({
|
||||
texcount: ctx.texcount,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,507 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/CompileController'
|
||||
)
|
||||
const Errors = require('../../../app/js/Errors')
|
||||
|
||||
describe('CompileController', function () {
|
||||
beforeEach(function () {
|
||||
this.buildId = 'build-id-123'
|
||||
this.CompileController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./CompileManager': (this.CompileManager = {}),
|
||||
'./RequestParser': (this.RequestParser = {}),
|
||||
'@overleaf/settings': (this.Settings = {
|
||||
apis: {
|
||||
clsi: {
|
||||
url: 'http://clsi.example.com',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
downloadHost: 'http://localhost:3013',
|
||||
},
|
||||
clsiCache: {
|
||||
enabled: false,
|
||||
url: 'http://localhost:3044',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'@overleaf/metrics': {
|
||||
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
||||
},
|
||||
'./ProjectPersistenceManager': (this.ProjectPersistenceManager = {}),
|
||||
'./CLSICacheHandler': {
|
||||
notifyCLSICacheAboutBuild: sinon.stub(),
|
||||
downloadLatestCompileCache: sinon.stub().resolves(),
|
||||
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
|
||||
},
|
||||
'./Errors': (this.Erros = Errors),
|
||||
},
|
||||
})
|
||||
this.Settings.externalUrl = 'http://www.example.com'
|
||||
this.req = {}
|
||||
this.res = {}
|
||||
this.next = sinon.stub()
|
||||
})
|
||||
|
||||
describe('compile', function () {
|
||||
beforeEach(function () {
|
||||
this.req.body = {
|
||||
compile: 'mock-body',
|
||||
}
|
||||
this.req.params = { project_id: (this.project_id = 'project-id-123') }
|
||||
this.request = {
|
||||
compile: 'mock-parsed-request',
|
||||
}
|
||||
this.request_with_project_id = {
|
||||
compile: this.request.compile,
|
||||
project_id: this.project_id,
|
||||
}
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
size: 1337,
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.RequestParser.parse = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, this.request)
|
||||
this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon
|
||||
.stub()
|
||||
.callsArg(1)
|
||||
this.stats = { foo: 1 }
|
||||
this.timings = { bar: 2 }
|
||||
this.res.status = sinon.stub().returnsThis()
|
||||
this.res.send = sinon.stub()
|
||||
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {
|
||||
outputFiles: this.output_files,
|
||||
buildId: this.buildId,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function () {
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should parse the request', function () {
|
||||
this.RequestParser.parse.calledWith(this.req.body).should.equal(true)
|
||||
})
|
||||
|
||||
it('should run the compile for the specified project', function () {
|
||||
this.CompileManager.doCompileWithLock
|
||||
.calledWith(this.request_with_project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should mark the project as accessed', function () {
|
||||
this.ProjectPersistenceManager.markProjectAsJustAccessed
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the JSON response', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'success',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
buildId: this.buildId,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a outputUrlPrefix', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings.apis.clsi.outputUrlPrefix = ''
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with empty outputUrlPrefix', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'success',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
buildId: this.buildId,
|
||||
outputUrlPrefix: '',
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with user provided fake_output.pdf', function () {
|
||||
beforeEach(function () {
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'fake_output.pdf',
|
||||
type: 'pdf',
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {
|
||||
outputFiles: this.output_files,
|
||||
buildId: this.buildId,
|
||||
})
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with status failure', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send.should.have.been.calledWith({
|
||||
compile: {
|
||||
status: 'failure',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
buildId: this.buildId,
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an empty output.pdf', function () {
|
||||
beforeEach(function () {
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
size: 0,
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {
|
||||
outputFiles: this.output_files,
|
||||
buildId: this.buildId,
|
||||
})
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with status failure', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send.should.have.been.calledWith({
|
||||
compile: {
|
||||
status: 'failure',
|
||||
error: null,
|
||||
stats: this.stats,
|
||||
buildId: this.buildId,
|
||||
timings: this.timings,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: this.output_files.map(file => ({
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an error', function () {
|
||||
beforeEach(function () {
|
||||
const error = new Error((this.message = 'error message'))
|
||||
error.buildId = this.buildId
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(error)
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the error', function () {
|
||||
this.res.status.calledWith(500).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'error',
|
||||
error: this.message,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
buildId: this.buildId,
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with too many compile requests error', function () {
|
||||
beforeEach(function () {
|
||||
const error = new Errors.TooManyCompileRequestsError(
|
||||
'too many concurrent compile requests'
|
||||
)
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(error)
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the error', function () {
|
||||
this.res.status.calledWith(503).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'unavailable',
|
||||
error: 'too many concurrent compile requests',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request times out', function () {
|
||||
beforeEach(function () {
|
||||
this.error = new Error((this.message = 'container timed out'))
|
||||
this.error.timedout = true
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(this.error)
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the timeout status', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: 'timedout',
|
||||
error: this.message,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request returns no output files', function () {
|
||||
beforeEach(function () {
|
||||
this.CompileManager.doCompileWithLock = sinon
|
||||
.stub()
|
||||
.callsFake((_req, stats, timings, cb) => {
|
||||
Object.assign(stats, this.stats)
|
||||
Object.assign(timings, this.timings)
|
||||
cb(null, {})
|
||||
})
|
||||
this.CompileController.compile(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should return the JSON response with the failure status', function () {
|
||||
this.res.status.calledWith(200).should.equal(true)
|
||||
this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
error: null,
|
||||
status: 'failure',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: [],
|
||||
stats: this.stats,
|
||||
timings: this.timings,
|
||||
// JSON.stringify will omit these undefined values
|
||||
buildId: undefined,
|
||||
clsiCacheShard: undefined,
|
||||
},
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromCode', function () {
|
||||
beforeEach(function () {
|
||||
this.file = 'main.tex'
|
||||
this.line = 42
|
||||
this.column = 5
|
||||
this.project_id = 'mock-project-id'
|
||||
this.req.params = { project_id: this.project_id }
|
||||
this.req.query = {
|
||||
file: this.file,
|
||||
line: this.line.toString(),
|
||||
column: this.column.toString(),
|
||||
}
|
||||
this.res.json = sinon.stub()
|
||||
|
||||
this.CompileManager.syncFromCode = sinon
|
||||
.stub()
|
||||
.yields(null, (this.pdfPositions = ['mock-positions']), true)
|
||||
this.CompileController.syncFromCode(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should find the corresponding location in the PDF', function () {
|
||||
this.CompileManager.syncFromCode
|
||||
.calledWith(
|
||||
this.project_id,
|
||||
undefined,
|
||||
this.file,
|
||||
this.line,
|
||||
this.column
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the positions', function () {
|
||||
this.res.json
|
||||
.calledWith({
|
||||
pdf: this.pdfPositions,
|
||||
downloadedFromCache: true,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromPdf', function () {
|
||||
beforeEach(function () {
|
||||
this.page = 5
|
||||
this.h = 100.23
|
||||
this.v = 45.67
|
||||
this.project_id = 'mock-project-id'
|
||||
this.req.params = { project_id: this.project_id }
|
||||
this.req.query = {
|
||||
page: this.page.toString(),
|
||||
h: this.h.toString(),
|
||||
v: this.v.toString(),
|
||||
}
|
||||
this.res.json = sinon.stub()
|
||||
|
||||
this.CompileManager.syncFromPdf = sinon
|
||||
.stub()
|
||||
.yields(null, (this.codePositions = ['mock-positions']), true)
|
||||
this.CompileController.syncFromPdf(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should find the corresponding location in the code', function () {
|
||||
this.CompileManager.syncFromPdf
|
||||
.calledWith(this.project_id, undefined, this.page, this.h, this.v)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the positions', function () {
|
||||
this.res.json
|
||||
.calledWith({
|
||||
code: this.codePositions,
|
||||
downloadedFromCache: true,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', function () {
|
||||
beforeEach(function () {
|
||||
this.file = 'main.tex'
|
||||
this.project_id = 'mock-project-id'
|
||||
this.req.params = { project_id: this.project_id }
|
||||
this.req.query = {
|
||||
file: this.file,
|
||||
image: (this.image = 'example.com/image'),
|
||||
}
|
||||
this.res.json = sinon.stub()
|
||||
|
||||
this.CompileManager.wordcount = sinon
|
||||
.stub()
|
||||
.callsArgWith(4, null, (this.texcount = ['mock-texcount']))
|
||||
})
|
||||
|
||||
it('should return the word count of a file', function () {
|
||||
this.CompileController.wordcount(this.req, this.res, this.next)
|
||||
this.CompileManager.wordcount
|
||||
.calledWith(this.project_id, undefined, this.file, this.image)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the texcount info', function () {
|
||||
this.CompileController.wordcount(this.req, this.res, this.next)
|
||||
this.res.json
|
||||
.calledWith({
|
||||
texcount: this.texcount,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,773 @@
|
||||
import { vi, expect, describe, beforeEach, it } from 'vitest'
|
||||
import Path from 'node:path'
|
||||
import sinon from 'sinon'
|
||||
import Metrics from '../../../app/js/Metrics.js'
|
||||
|
||||
const MODULE_PATH = Path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/CompileManager'
|
||||
)
|
||||
|
||||
describe('CompileManager', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.projectId = 'project-id-123'
|
||||
ctx.userId = '1234'
|
||||
ctx.resources = 'mock-resources'
|
||||
ctx.outputFiles = [
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
},
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
]
|
||||
ctx.buildFiles = [
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
ctx.buildId = '00000000000-0000000000000000'
|
||||
ctx.commandOutput = 'Dummy output'
|
||||
ctx.compileBaseDir = '/compile/dir'
|
||||
ctx.outputBaseDir = '/output/dir'
|
||||
ctx.compileDir = `${ctx.compileBaseDir}/${ctx.projectId}-${ctx.userId}`
|
||||
ctx.outputDir = `${ctx.outputBaseDir}/${ctx.projectId}-${ctx.userId}`
|
||||
|
||||
ctx.LatexRunner = {
|
||||
promises: {
|
||||
runLatex: sinon.stub().resolves({}),
|
||||
},
|
||||
}
|
||||
ctx.ResourceWriter = {
|
||||
promises: {
|
||||
syncResourcesToDisk: sinon.stub().resolves(ctx.resources),
|
||||
},
|
||||
}
|
||||
ctx.OutputFileFinder = {
|
||||
promises: {
|
||||
findOutputFiles: sinon.stub().resolves({
|
||||
outputFiles: ctx.outputFiles,
|
||||
allEntries: ctx.outputFiles.map(f => f.path).concat(['main.tex']),
|
||||
}),
|
||||
},
|
||||
}
|
||||
ctx.OutputCacheManager = {
|
||||
BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/,
|
||||
CACHE_SUBDIR: 'generated-files',
|
||||
promises: {
|
||||
queueDirOperation: sinon.stub().callsArg(1),
|
||||
saveOutputFiles: sinon
|
||||
.stub()
|
||||
.resolves({ outputFiles: ctx.buildFiles, buildId: ctx.buildId }),
|
||||
},
|
||||
}
|
||||
ctx.Settings = {
|
||||
path: {
|
||||
compilesDir: ctx.compileBaseDir,
|
||||
outputDir: ctx.outputBaseDir,
|
||||
synctexBaseDir: sinon.stub(),
|
||||
},
|
||||
clsi: {
|
||||
docker: {
|
||||
image: 'SOMEIMAGE',
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx.Settings.path.synctexBaseDir
|
||||
.withArgs(`${ctx.projectId}-${ctx.userId}`)
|
||||
.returns(ctx.compileDir)
|
||||
ctx.child_process = {
|
||||
exec: sinon.stub(),
|
||||
execFile: sinon.stub().yields(),
|
||||
}
|
||||
ctx.CommandRunner = {
|
||||
canRunSyncTeXInOutputDir: sinon.stub().returns(false),
|
||||
promises: {
|
||||
run: sinon.stub().callsFake((_1, _2, _3, _4, _5, _6, compileGroup) => {
|
||||
if (compileGroup === 'synctex' || compileGroup === 'synctex-output') {
|
||||
return Promise.resolve({ stdout: ctx.commandOutput })
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
stdout: 'Encoding: ascii\nWords in text: 2',
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
ctx.DraftModeManager = {
|
||||
promises: {
|
||||
injectDraftMode: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.TikzManager = {
|
||||
promises: {
|
||||
checkMainFile: sinon.stub().resolves(false),
|
||||
},
|
||||
}
|
||||
ctx.lock = {
|
||||
release: sinon.stub(),
|
||||
}
|
||||
ctx.LockManager = {
|
||||
acquire: sinon.stub().returns(ctx.lock),
|
||||
}
|
||||
ctx.SynctexOutputParser = {
|
||||
parseViewOutput: sinon.stub(),
|
||||
parseEditOutput: sinon.stub(),
|
||||
}
|
||||
|
||||
ctx.dirStats = {
|
||||
isDirectory: sinon.stub().returns(true),
|
||||
}
|
||||
ctx.fileStats = {
|
||||
isFile: sinon.stub().returns(true),
|
||||
}
|
||||
ctx.fsPromises = {
|
||||
lstat: sinon.stub(),
|
||||
stat: sinon.stub(),
|
||||
readFile: sinon.stub(),
|
||||
mkdir: sinon.stub().resolves(),
|
||||
rm: sinon.stub().resolves(),
|
||||
unlink: sinon.stub().resolves(),
|
||||
rmdir: sinon.stub().resolves(),
|
||||
}
|
||||
ctx.fsPromises.lstat.withArgs(ctx.compileDir).resolves(ctx.dirStats)
|
||||
ctx.fsPromises.stat
|
||||
.withArgs(Path.join(ctx.compileDir, 'output.synctex.gz'))
|
||||
.resolves(ctx.fileStats)
|
||||
|
||||
ctx.CLSICacheHandler = {
|
||||
notifyCLSICacheAboutBuild: sinon.stub(),
|
||||
downloadLatestCompileCache: sinon.stub().resolves(),
|
||||
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
ctx.LatexMetrics = { enableLatexMkMetrics: sinon.stub() }
|
||||
|
||||
ctx.StatsManager = { sampleRequest: sinon.stub().returns(false) }
|
||||
|
||||
vi.doMock('../../../app/js/LatexRunner', () => ({
|
||||
default: ctx.LatexRunner,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/ResourceWriter', () => ({
|
||||
default: ctx.ResourceWriter,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/OutputFileFinder', () => ({
|
||||
default: ctx.OutputFileFinder,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/OutputCacheManager', () => ({
|
||||
default: ctx.OutputCacheManager,
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: ctx.Settings,
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: {
|
||||
inc: sinon.stub(),
|
||||
timing: sinon.stub(),
|
||||
gauge: sinon.stub(),
|
||||
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('child_process', () => ({
|
||||
default: ctx.child_process,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/CommandRunner', () => ({
|
||||
default: ctx.CommandRunner,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/DraftModeManager', () => ({
|
||||
default: ctx.DraftModeManager,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/TikzManager', () => ({
|
||||
default: ctx.TikzManager,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/LockManager', () => ({
|
||||
default: ctx.LockManager,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/SynctexOutputParser', () => ({
|
||||
default: ctx.SynctexOutputParser,
|
||||
}))
|
||||
|
||||
vi.doMock('fs/promises', () => ({
|
||||
default: ctx.fsPromises,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/CLSICacheHandler', () => ({
|
||||
default: ctx.CLSICacheHandler,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/LatexMetrics', () => ({
|
||||
default: ctx.LatexMetrics,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/StatsManager', () => ({
|
||||
default: ctx.StatsManager,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/Metrics', () => ({
|
||||
default: Metrics,
|
||||
}))
|
||||
|
||||
ctx.CompileManager = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
describe('doCompileWithLock', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.request = {
|
||||
resources: ctx.resources,
|
||||
rootResourcePath: (ctx.rootResourcePath = 'main.tex'),
|
||||
project_id: ctx.projectId,
|
||||
user_id: ctx.userId,
|
||||
compiler: (ctx.compiler = 'pdflatex'),
|
||||
timeout: (ctx.timeout = 42000),
|
||||
imageName: (ctx.image = 'example.com/image'),
|
||||
flags: (ctx.flags = ['-file-line-error']),
|
||||
compileGroup: (ctx.compileGroup = 'compile-group'),
|
||||
stopOnFirstError: false,
|
||||
metricsOpts: {
|
||||
path: 'clsi-perf',
|
||||
method: 'minimal',
|
||||
compile: 'initial',
|
||||
},
|
||||
}
|
||||
ctx.env = {
|
||||
OVERLEAF_PROJECT_ID: ctx.projectId,
|
||||
}
|
||||
})
|
||||
|
||||
describe('when the project is locked', () => {
|
||||
beforeEach(async ctx => {
|
||||
const error = new Error('locked')
|
||||
ctx.LockManager.acquire.throws(error)
|
||||
await expect(
|
||||
ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
).to.be.rejectedWith(error)
|
||||
})
|
||||
|
||||
it('should ensure that the compile directory exists', ctx => {
|
||||
expect(ctx.fsPromises.mkdir).to.have.been.calledWith(ctx.compileDir, {
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not run LaTeX', ctx => {
|
||||
expect(ctx.LatexRunner.promises.runLatex).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('normally', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.result = await ctx.CompileManager.promises.doCompileWithLock(
|
||||
ctx.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure that the compile directory exists', ctx => {
|
||||
expect(ctx.fsPromises.mkdir).to.have.been.calledWith(ctx.compileDir, {
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should write the resources to disk', ctx => {
|
||||
expect(
|
||||
ctx.ResourceWriter.promises.syncResourcesToDisk
|
||||
).to.have.been.calledWith(ctx.request, ctx.compileDir)
|
||||
})
|
||||
|
||||
it('should run LaTeX', ctx => {
|
||||
expect(ctx.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
{
|
||||
directory: ctx.compileDir,
|
||||
mainFile: ctx.rootResourcePath,
|
||||
compiler: ctx.compiler,
|
||||
timeout: ctx.timeout,
|
||||
image: ctx.image,
|
||||
flags: ctx.flags,
|
||||
environment: ctx.env,
|
||||
compileGroup: ctx.compileGroup,
|
||||
stopOnFirstError: ctx.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should find the output files', ctx => {
|
||||
expect(
|
||||
ctx.OutputFileFinder.promises.findOutputFiles
|
||||
).to.have.been.calledWith(ctx.resources, ctx.compileDir)
|
||||
})
|
||||
|
||||
it('should return the output files', ctx => {
|
||||
expect(ctx.result.outputFiles).to.equal(ctx.buildFiles)
|
||||
})
|
||||
|
||||
it('should not inject draft mode by default', ctx => {
|
||||
expect(ctx.DraftModeManager.promises.injectDraftMode).not.to.have.been
|
||||
.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('with performance metric collection', () => {
|
||||
it('should enable latexmk metrics when sampleRequest returns true', async ctx => {
|
||||
ctx.StatsManager.sampleRequest.returns(true)
|
||||
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
expect(ctx.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
||||
sinon.match.object
|
||||
)
|
||||
})
|
||||
|
||||
it('should enable latexmk metrics when sampleRequest returns false', async ctx => {
|
||||
ctx.StatsManager.sampleRequest.returns(false)
|
||||
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
expect(ctx.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
||||
sinon.match.object
|
||||
)
|
||||
})
|
||||
|
||||
it('should enable latexmk metrics when sampleRequest returns undefined', async ctx => {
|
||||
ctx.StatsManager.sampleRequest.returns(undefined)
|
||||
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
expect(ctx.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
||||
sinon.match.object
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with draft mode', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.request.draft = true
|
||||
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
})
|
||||
|
||||
it('should inject the draft mode header', ctx => {
|
||||
expect(
|
||||
ctx.DraftModeManager.promises.injectDraftMode
|
||||
).to.have.been.calledWith(ctx.compileDir + '/' + ctx.rootResourcePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a check option', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.request.check = 'error'
|
||||
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
})
|
||||
|
||||
it('should run chktex', ctx => {
|
||||
expect(ctx.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
{
|
||||
directory: ctx.compileDir,
|
||||
mainFile: ctx.rootResourcePath,
|
||||
compiler: ctx.compiler,
|
||||
timeout: ctx.timeout,
|
||||
image: ctx.image,
|
||||
flags: ctx.flags,
|
||||
environment: {
|
||||
CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16',
|
||||
CHKTEX_EXIT_ON_ERROR: 1,
|
||||
CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000',
|
||||
OVERLEAF_PROJECT_ID: ctx.projectId,
|
||||
},
|
||||
compileGroup: ctx.compileGroup,
|
||||
stopOnFirstError: ctx.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a knitr file and check options', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.request.rootResourcePath = 'main.Rtex'
|
||||
ctx.request.check = 'error'
|
||||
await ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
})
|
||||
|
||||
it('should not run chktex', ctx => {
|
||||
expect(ctx.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
{
|
||||
directory: ctx.compileDir,
|
||||
mainFile: 'main.Rtex',
|
||||
compiler: ctx.compiler,
|
||||
timeout: ctx.timeout,
|
||||
image: ctx.image,
|
||||
flags: ctx.flags,
|
||||
environment: ctx.env,
|
||||
compileGroup: ctx.compileGroup,
|
||||
stopOnFirstError: ctx.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the compile times out', () => {
|
||||
beforeEach(async ctx => {
|
||||
const error = new Error('timed out!')
|
||||
error.timedout = true
|
||||
ctx.LatexRunner.promises.runLatex.rejects(error)
|
||||
await expect(
|
||||
ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should clear the compile directory', ctx => {
|
||||
for (const { path } of ctx.buildFiles) {
|
||||
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
||||
ctx.compileDir + '/' + path
|
||||
)
|
||||
}
|
||||
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
||||
ctx.compileDir + '/main.tex'
|
||||
)
|
||||
expect(ctx.fsPromises.rmdir).to.have.been.calledWith(ctx.compileDir)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the compile is manually stopped', () => {
|
||||
beforeEach(async ctx => {
|
||||
const error = new Error('terminated!')
|
||||
error.terminated = true
|
||||
ctx.LatexRunner.promises.runLatex.rejects(error)
|
||||
await expect(
|
||||
ctx.CompileManager.promises.doCompileWithLock(ctx.request, {}, {})
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should clear the compile directory', ctx => {
|
||||
for (const { path } of ctx.buildFiles) {
|
||||
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
||||
ctx.compileDir + '/' + path
|
||||
)
|
||||
}
|
||||
expect(ctx.fsPromises.unlink).to.have.been.calledWith(
|
||||
ctx.compileDir + '/main.tex'
|
||||
)
|
||||
expect(ctx.fsPromises.rmdir).to.have.been.calledWith(ctx.compileDir)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearProject', () => {
|
||||
it('should clear the compile directory', async ctx => {
|
||||
await ctx.CompileManager.promises.clearProject(ctx.projectId, ctx.userId)
|
||||
|
||||
expect(ctx.fsPromises.rm).to.have.been.calledWith(ctx.compileDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncing', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.page = 1
|
||||
ctx.h = 42.23
|
||||
ctx.v = 87.56
|
||||
ctx.width = 100.01
|
||||
ctx.height = 234.56
|
||||
ctx.line = 5
|
||||
ctx.column = 3
|
||||
ctx.filename = 'main.tex'
|
||||
})
|
||||
|
||||
describe('syncFromCode', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.records = [{ page: 1, h: 2, v: 3, width: 4, height: 5 }]
|
||||
ctx.SynctexOutputParser.parseViewOutput
|
||||
.withArgs(ctx.commandOutput)
|
||||
.returns(ctx.records)
|
||||
})
|
||||
|
||||
describe('normal case', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.result = await ctx.CompileManager.promises.syncFromCode(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
ctx.filename,
|
||||
ctx.line,
|
||||
ctx.column,
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary', ctx => {
|
||||
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
||||
const inputFilePath = `${ctx.compileDir}/${ctx.filename}`
|
||||
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${ctx.line}:${ctx.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
ctx.compileDir,
|
||||
ctx.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{},
|
||||
'synctex'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', ctx => {
|
||||
expect(ctx.result).to.deep.equal({
|
||||
codePositions: ctx.records,
|
||||
downloadedFromCache: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('from cache in docker', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.CommandRunner.canRunSyncTeXInOutputDir.returns(true)
|
||||
ctx.Settings.path.synctexBaseDir
|
||||
.withArgs(`${ctx.projectId}-${ctx.userId}`)
|
||||
.returns('/compile')
|
||||
|
||||
const errNotFound = new Error()
|
||||
errNotFound.code = 'ENOENT'
|
||||
ctx.outputDir = `${ctx.outputBaseDir}/${ctx.projectId}-${ctx.userId}/${ctx.OutputCacheManager.CACHE_SUBDIR}/${ctx.buildId}`
|
||||
const filename = Path.join(ctx.outputDir, 'output.synctex.gz')
|
||||
ctx.fsPromises.stat
|
||||
.withArgs(ctx.outputDir)
|
||||
.onFirstCall()
|
||||
.rejects(errNotFound)
|
||||
ctx.fsPromises.stat
|
||||
.withArgs(ctx.outputDir)
|
||||
.onSecondCall()
|
||||
.resolves(ctx.dirStats)
|
||||
ctx.fsPromises.stat.withArgs(filename).resolves(ctx.fileStats)
|
||||
ctx.CLSICacheHandler.downloadOutputDotSynctexFromCompileCache.resolves(
|
||||
true
|
||||
)
|
||||
ctx.result = await ctx.CompileManager.promises.syncFromCode(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
ctx.filename,
|
||||
ctx.line,
|
||||
ctx.column,
|
||||
{
|
||||
imageName: 'image',
|
||||
editorId: '00000000-0000-0000-0000-000000000000',
|
||||
buildId: ctx.buildId,
|
||||
compileFromClsiCache: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should run in output dir', ctx => {
|
||||
const outputFilePath = '/compile/output.pdf'
|
||||
const inputFilePath = `/compile/${ctx.filename}`
|
||||
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${ctx.line}:${ctx.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
ctx.outputDir,
|
||||
'image',
|
||||
60000,
|
||||
{},
|
||||
'synctex-output'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', ctx => {
|
||||
expect(ctx.result).to.deep.equal({
|
||||
codePositions: ctx.records,
|
||||
downloadedFromCache: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a custom imageName', () => {
|
||||
const customImageName = 'foo/bar:tag-0'
|
||||
beforeEach(async ctx => {
|
||||
await ctx.CompileManager.promises.syncFromCode(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
ctx.filename,
|
||||
ctx.line,
|
||||
ctx.column,
|
||||
{ imageName: customImageName }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary in a custom docker image', ctx => {
|
||||
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
||||
const inputFilePath = `${ctx.compileDir}/${ctx.filename}`
|
||||
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${ctx.line}:${ctx.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
ctx.compileDir,
|
||||
customImageName,
|
||||
60000,
|
||||
{},
|
||||
'synctex'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromPdf', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.records = [{ file: 'main.tex', line: 1, column: 1 }]
|
||||
ctx.SynctexOutputParser.parseEditOutput
|
||||
.withArgs(ctx.commandOutput, ctx.compileDir)
|
||||
.returns(ctx.records)
|
||||
})
|
||||
|
||||
describe('normal case', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.result = await ctx.CompileManager.promises.syncFromPdf(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
ctx.page,
|
||||
ctx.h,
|
||||
ctx.v,
|
||||
{ imageName: '' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary', ctx => {
|
||||
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
||||
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'edit',
|
||||
'-o',
|
||||
`${ctx.page}:${ctx.h}:${ctx.v}:${outputFilePath}`,
|
||||
],
|
||||
ctx.compileDir,
|
||||
ctx.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', ctx => {
|
||||
expect(ctx.result).to.deep.equal({
|
||||
pdfPositions: ctx.records,
|
||||
downloadedFromCache: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a custom imageName', () => {
|
||||
const customImageName = 'foo/bar:tag-1'
|
||||
beforeEach(async ctx => {
|
||||
await ctx.CompileManager.promises.syncFromPdf(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
ctx.page,
|
||||
ctx.h,
|
||||
ctx.v,
|
||||
{ imageName: customImageName }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary in a custom docker image', ctx => {
|
||||
const outputFilePath = `${ctx.compileDir}/output.pdf`
|
||||
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'edit',
|
||||
'-o',
|
||||
`${ctx.page}:${ctx.h}:${ctx.v}:${outputFilePath}`,
|
||||
],
|
||||
ctx.compileDir,
|
||||
customImageName,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.timeout = 60 * 1000
|
||||
ctx.filename = 'main.tex'
|
||||
ctx.image = 'example.com/image'
|
||||
|
||||
ctx.result = await ctx.CompileManager.promises.wordcount(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
ctx.filename,
|
||||
ctx.image
|
||||
)
|
||||
})
|
||||
|
||||
it('should run the texcount command', ctx => {
|
||||
ctx.filePath = `$COMPILE_DIR/${ctx.filename}`
|
||||
ctx.command = ['texcount', '-nocol', '-inc', ctx.filePath]
|
||||
|
||||
expect(ctx.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${ctx.projectId}-${ctx.userId}`,
|
||||
ctx.command,
|
||||
ctx.compileDir,
|
||||
ctx.image,
|
||||
ctx.timeout,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', ctx => {
|
||||
expect(ctx.result).to.deep.equal({
|
||||
encode: 'ascii',
|
||||
textWords: 2,
|
||||
headWords: 0,
|
||||
outside: 0,
|
||||
headers: 0,
|
||||
elements: 0,
|
||||
mathInline: 0,
|
||||
mathDisplay: 0,
|
||||
errors: 0,
|
||||
messages: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,753 +0,0 @@
|
||||
const Path = require('node:path')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const Metrics = require('../../../app/js/Metrics')
|
||||
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/CompileManager'
|
||||
)
|
||||
|
||||
describe('CompileManager', function () {
|
||||
beforeEach(function () {
|
||||
this.projectId = 'project-id-123'
|
||||
this.userId = '1234'
|
||||
this.resources = 'mock-resources'
|
||||
this.outputFiles = [
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
},
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
]
|
||||
this.buildFiles = [
|
||||
{
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
build: 1234,
|
||||
},
|
||||
]
|
||||
this.buildId = '00000000000-0000000000000000'
|
||||
this.commandOutput = 'Dummy output'
|
||||
this.compileBaseDir = '/compile/dir'
|
||||
this.outputBaseDir = '/output/dir'
|
||||
this.compileDir = `${this.compileBaseDir}/${this.projectId}-${this.userId}`
|
||||
this.outputDir = `${this.outputBaseDir}/${this.projectId}-${this.userId}`
|
||||
|
||||
this.LatexRunner = {
|
||||
promises: {
|
||||
runLatex: sinon.stub().resolves({}),
|
||||
},
|
||||
}
|
||||
this.ResourceWriter = {
|
||||
promises: {
|
||||
syncResourcesToDisk: sinon.stub().resolves(this.resources),
|
||||
},
|
||||
}
|
||||
this.OutputFileFinder = {
|
||||
promises: {
|
||||
findOutputFiles: sinon.stub().resolves({
|
||||
outputFiles: this.outputFiles,
|
||||
allEntries: this.outputFiles.map(f => f.path).concat(['main.tex']),
|
||||
}),
|
||||
},
|
||||
}
|
||||
this.OutputCacheManager = {
|
||||
BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/,
|
||||
CACHE_SUBDIR: 'generated-files',
|
||||
promises: {
|
||||
queueDirOperation: sinon.stub().callsArg(1),
|
||||
saveOutputFiles: sinon
|
||||
.stub()
|
||||
.resolves({ outputFiles: this.buildFiles, buildId: this.buildId }),
|
||||
},
|
||||
}
|
||||
this.Settings = {
|
||||
path: {
|
||||
compilesDir: this.compileBaseDir,
|
||||
outputDir: this.outputBaseDir,
|
||||
synctexBaseDir: sinon.stub(),
|
||||
},
|
||||
clsi: {
|
||||
docker: {
|
||||
image: 'SOMEIMAGE',
|
||||
},
|
||||
},
|
||||
}
|
||||
this.Settings.path.synctexBaseDir
|
||||
.withArgs(`${this.projectId}-${this.userId}`)
|
||||
.returns(this.compileDir)
|
||||
this.child_process = {
|
||||
exec: sinon.stub(),
|
||||
execFile: sinon.stub().yields(),
|
||||
}
|
||||
this.CommandRunner = {
|
||||
canRunSyncTeXInOutputDir: sinon.stub().returns(false),
|
||||
promises: {
|
||||
run: sinon.stub().callsFake((_1, _2, _3, _4, _5, _6, compileGroup) => {
|
||||
if (compileGroup === 'synctex' || compileGroup === 'synctex-output') {
|
||||
return Promise.resolve({ stdout: this.commandOutput })
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
stdout: 'Encoding: ascii\nWords in text: 2',
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
this.DraftModeManager = {
|
||||
promises: {
|
||||
injectDraftMode: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.TikzManager = {
|
||||
promises: {
|
||||
checkMainFile: sinon.stub().resolves(false),
|
||||
},
|
||||
}
|
||||
this.lock = {
|
||||
release: sinon.stub(),
|
||||
}
|
||||
this.LockManager = {
|
||||
acquire: sinon.stub().returns(this.lock),
|
||||
}
|
||||
this.SynctexOutputParser = {
|
||||
parseViewOutput: sinon.stub(),
|
||||
parseEditOutput: sinon.stub(),
|
||||
}
|
||||
|
||||
this.dirStats = {
|
||||
isDirectory: sinon.stub().returns(true),
|
||||
}
|
||||
this.fileStats = {
|
||||
isFile: sinon.stub().returns(true),
|
||||
}
|
||||
this.fsPromises = {
|
||||
lstat: sinon.stub(),
|
||||
stat: sinon.stub(),
|
||||
readFile: sinon.stub(),
|
||||
mkdir: sinon.stub().resolves(),
|
||||
rm: sinon.stub().resolves(),
|
||||
unlink: sinon.stub().resolves(),
|
||||
rmdir: sinon.stub().resolves(),
|
||||
}
|
||||
this.fsPromises.lstat.withArgs(this.compileDir).resolves(this.dirStats)
|
||||
this.fsPromises.stat
|
||||
.withArgs(Path.join(this.compileDir, 'output.synctex.gz'))
|
||||
.resolves(this.fileStats)
|
||||
|
||||
this.CLSICacheHandler = {
|
||||
notifyCLSICacheAboutBuild: sinon.stub(),
|
||||
downloadLatestCompileCache: sinon.stub().resolves(),
|
||||
downloadOutputDotSynctexFromCompileCache: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
this.LatexMetrics = { enableLatexMkMetrics: sinon.stub() }
|
||||
|
||||
this.StatsManager = { sampleRequest: sinon.stub().returns(false) }
|
||||
|
||||
this.CompileManager = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'./LatexRunner': this.LatexRunner,
|
||||
'./ResourceWriter': this.ResourceWriter,
|
||||
'./OutputFileFinder': this.OutputFileFinder,
|
||||
'./OutputCacheManager': this.OutputCacheManager,
|
||||
'@overleaf/settings': this.Settings,
|
||||
'@overleaf/metrics': {
|
||||
inc: sinon.stub(),
|
||||
timing: sinon.stub(),
|
||||
gauge: sinon.stub(),
|
||||
Timer: sinon.stub().returns({ done: sinon.stub() }),
|
||||
},
|
||||
child_process: this.child_process,
|
||||
'./CommandRunner': this.CommandRunner,
|
||||
'./DraftModeManager': this.DraftModeManager,
|
||||
'./TikzManager': this.TikzManager,
|
||||
'./LockManager': this.LockManager,
|
||||
'./SynctexOutputParser': this.SynctexOutputParser,
|
||||
'fs/promises': this.fsPromises,
|
||||
'./CLSICacheHandler': this.CLSICacheHandler,
|
||||
'./LatexMetrics': this.LatexMetrics,
|
||||
'./StatsManager': this.StatsManager,
|
||||
'./Metrics': Metrics,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('doCompileWithLock', function () {
|
||||
beforeEach(function () {
|
||||
this.request = {
|
||||
resources: this.resources,
|
||||
rootResourcePath: (this.rootResourcePath = 'main.tex'),
|
||||
project_id: this.projectId,
|
||||
user_id: this.userId,
|
||||
compiler: (this.compiler = 'pdflatex'),
|
||||
timeout: (this.timeout = 42000),
|
||||
imageName: (this.image = 'example.com/image'),
|
||||
flags: (this.flags = ['-file-line-error']),
|
||||
compileGroup: (this.compileGroup = 'compile-group'),
|
||||
stopOnFirstError: false,
|
||||
metricsOpts: {
|
||||
path: 'clsi-perf',
|
||||
method: 'minimal',
|
||||
compile: 'initial',
|
||||
},
|
||||
}
|
||||
this.env = {
|
||||
OVERLEAF_PROJECT_ID: this.projectId,
|
||||
}
|
||||
})
|
||||
|
||||
describe('when the project is locked', function () {
|
||||
beforeEach(async function () {
|
||||
const error = new Error('locked')
|
||||
this.LockManager.acquire.throws(error)
|
||||
await expect(
|
||||
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
|
||||
).to.be.rejectedWith(error)
|
||||
})
|
||||
|
||||
it('should ensure that the compile directory exists', function () {
|
||||
expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not run LaTeX', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('normally', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure that the compile directory exists', function () {
|
||||
expect(this.fsPromises.mkdir).to.have.been.calledWith(this.compileDir, {
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should write the resources to disk', function () {
|
||||
expect(
|
||||
this.ResourceWriter.promises.syncResourcesToDisk
|
||||
).to.have.been.calledWith(this.request, this.compileDir)
|
||||
})
|
||||
|
||||
it('should run LaTeX', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
{
|
||||
directory: this.compileDir,
|
||||
mainFile: this.rootResourcePath,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: this.env,
|
||||
compileGroup: this.compileGroup,
|
||||
stopOnFirstError: this.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should find the output files', function () {
|
||||
expect(
|
||||
this.OutputFileFinder.promises.findOutputFiles
|
||||
).to.have.been.calledWith(this.resources, this.compileDir)
|
||||
})
|
||||
|
||||
it('should return the output files', function () {
|
||||
expect(this.result.outputFiles).to.equal(this.buildFiles)
|
||||
})
|
||||
|
||||
it('should not inject draft mode by default', function () {
|
||||
expect(this.DraftModeManager.promises.injectDraftMode).not.to.have.been
|
||||
.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('with performance metric collection', function () {
|
||||
it('should enable latexmk metrics when sampleRequest returns true', async function () {
|
||||
this.StatsManager.sampleRequest.returns(true)
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
expect(this.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
||||
sinon.match.object
|
||||
)
|
||||
})
|
||||
|
||||
it('should enable latexmk metrics when sampleRequest returns false', async function () {
|
||||
this.StatsManager.sampleRequest.returns(false)
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
expect(this.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
||||
sinon.match.object
|
||||
)
|
||||
})
|
||||
|
||||
it('should enable latexmk metrics when sampleRequest returns undefined', async function () {
|
||||
this.StatsManager.sampleRequest.returns(undefined)
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
expect(this.LatexMetrics.enableLatexMkMetrics).to.have.been.calledWith(
|
||||
sinon.match.object
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with draft mode', function () {
|
||||
beforeEach(async function () {
|
||||
this.request.draft = true
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should inject the draft mode header', function () {
|
||||
expect(
|
||||
this.DraftModeManager.promises.injectDraftMode
|
||||
).to.have.been.calledWith(this.compileDir + '/' + this.rootResourcePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a check option', function () {
|
||||
beforeEach(async function () {
|
||||
this.request.check = 'error'
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should run chktex', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
{
|
||||
directory: this.compileDir,
|
||||
mainFile: this.rootResourcePath,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: {
|
||||
CHKTEX_OPTIONS: '-nall -e9 -e10 -w15 -w16',
|
||||
CHKTEX_EXIT_ON_ERROR: 1,
|
||||
CHKTEX_ULIMIT_OPTIONS: '-t 5 -v 64000',
|
||||
OVERLEAF_PROJECT_ID: this.projectId,
|
||||
},
|
||||
compileGroup: this.compileGroup,
|
||||
stopOnFirstError: this.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a knitr file and check options', function () {
|
||||
beforeEach(async function () {
|
||||
this.request.rootResourcePath = 'main.Rtex'
|
||||
this.request.check = 'error'
|
||||
await this.CompileManager.promises.doCompileWithLock(
|
||||
this.request,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not run chktex', function () {
|
||||
expect(this.LatexRunner.promises.runLatex).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
{
|
||||
directory: this.compileDir,
|
||||
mainFile: 'main.Rtex',
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: this.env,
|
||||
compileGroup: this.compileGroup,
|
||||
stopOnFirstError: this.request.stopOnFirstError,
|
||||
stats: sinon.match.object,
|
||||
timings: sinon.match.object,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the compile times out', function () {
|
||||
beforeEach(async function () {
|
||||
const error = new Error('timed out!')
|
||||
error.timedout = true
|
||||
this.LatexRunner.promises.runLatex.rejects(error)
|
||||
await expect(
|
||||
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should clear the compile directory', function () {
|
||||
for (const { path } of this.buildFiles) {
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/' + path
|
||||
)
|
||||
}
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/main.tex'
|
||||
)
|
||||
expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the compile is manually stopped', function () {
|
||||
beforeEach(async function () {
|
||||
const error = new Error('terminated!')
|
||||
error.terminated = true
|
||||
this.LatexRunner.promises.runLatex.rejects(error)
|
||||
await expect(
|
||||
this.CompileManager.promises.doCompileWithLock(this.request, {}, {})
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should clear the compile directory', function () {
|
||||
for (const { path } of this.buildFiles) {
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/' + path
|
||||
)
|
||||
}
|
||||
expect(this.fsPromises.unlink).to.have.been.calledWith(
|
||||
this.compileDir + '/main.tex'
|
||||
)
|
||||
expect(this.fsPromises.rmdir).to.have.been.calledWith(this.compileDir)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearProject', function () {
|
||||
it('should clear the compile directory', async function () {
|
||||
await this.CompileManager.promises.clearProject(
|
||||
this.projectId,
|
||||
this.userId
|
||||
)
|
||||
|
||||
expect(this.fsPromises.rm).to.have.been.calledWith(this.compileDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncing', function () {
|
||||
beforeEach(function () {
|
||||
this.page = 1
|
||||
this.h = 42.23
|
||||
this.v = 87.56
|
||||
this.width = 100.01
|
||||
this.height = 234.56
|
||||
this.line = 5
|
||||
this.column = 3
|
||||
this.filename = 'main.tex'
|
||||
})
|
||||
|
||||
describe('syncFromCode', function () {
|
||||
beforeEach(function () {
|
||||
this.records = [{ page: 1, h: 2, v: 3, width: 4, height: 5 }]
|
||||
this.SynctexOutputParser.parseViewOutput
|
||||
.withArgs(this.commandOutput)
|
||||
.returns(this.records)
|
||||
})
|
||||
|
||||
describe('normal case', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await this.CompileManager.promises.syncFromCode(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.filename,
|
||||
this.line,
|
||||
this.column,
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
const inputFilePath = `${this.compileDir}/${this.filename}`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${this.line}:${this.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
this.compileDir,
|
||||
this.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{},
|
||||
'synctex'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', function () {
|
||||
expect(this.result).to.deep.equal({
|
||||
codePositions: this.records,
|
||||
downloadedFromCache: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('from cache in docker', function () {
|
||||
beforeEach(async function () {
|
||||
this.CommandRunner.canRunSyncTeXInOutputDir.returns(true)
|
||||
this.Settings.path.synctexBaseDir
|
||||
.withArgs(`${this.projectId}-${this.userId}`)
|
||||
.returns('/compile')
|
||||
|
||||
const errNotFound = new Error()
|
||||
errNotFound.code = 'ENOENT'
|
||||
this.outputDir = `${this.outputBaseDir}/${this.projectId}-${this.userId}/${this.OutputCacheManager.CACHE_SUBDIR}/${this.buildId}`
|
||||
const filename = Path.join(this.outputDir, 'output.synctex.gz')
|
||||
this.fsPromises.stat
|
||||
.withArgs(this.outputDir)
|
||||
.onFirstCall()
|
||||
.rejects(errNotFound)
|
||||
this.fsPromises.stat
|
||||
.withArgs(this.outputDir)
|
||||
.onSecondCall()
|
||||
.resolves(this.dirStats)
|
||||
this.fsPromises.stat.withArgs(filename).resolves(this.fileStats)
|
||||
this.CLSICacheHandler.downloadOutputDotSynctexFromCompileCache.resolves(
|
||||
true
|
||||
)
|
||||
this.result = await this.CompileManager.promises.syncFromCode(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.filename,
|
||||
this.line,
|
||||
this.column,
|
||||
{
|
||||
imageName: 'image',
|
||||
editorId: '00000000-0000-0000-0000-000000000000',
|
||||
buildId: this.buildId,
|
||||
compileFromClsiCache: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should run in output dir', function () {
|
||||
const outputFilePath = '/compile/output.pdf'
|
||||
const inputFilePath = `/compile/${this.filename}`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${this.line}:${this.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
this.outputDir,
|
||||
'image',
|
||||
60000,
|
||||
{},
|
||||
'synctex-output'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', function () {
|
||||
expect(this.result).to.deep.equal({
|
||||
codePositions: this.records,
|
||||
downloadedFromCache: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a custom imageName', function () {
|
||||
const customImageName = 'foo/bar:tag-0'
|
||||
beforeEach(async function () {
|
||||
await this.CompileManager.promises.syncFromCode(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.filename,
|
||||
this.line,
|
||||
this.column,
|
||||
{ imageName: customImageName }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary in a custom docker image', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
const inputFilePath = `${this.compileDir}/${this.filename}`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'view',
|
||||
'-i',
|
||||
`${this.line}:${this.column}:${inputFilePath}`,
|
||||
'-o',
|
||||
outputFilePath,
|
||||
],
|
||||
this.compileDir,
|
||||
customImageName,
|
||||
60000,
|
||||
{},
|
||||
'synctex'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncFromPdf', function () {
|
||||
beforeEach(function () {
|
||||
this.records = [{ file: 'main.tex', line: 1, column: 1 }]
|
||||
this.SynctexOutputParser.parseEditOutput
|
||||
.withArgs(this.commandOutput, this.compileDir)
|
||||
.returns(this.records)
|
||||
})
|
||||
|
||||
describe('normal case', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await this.CompileManager.promises.syncFromPdf(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.page,
|
||||
this.h,
|
||||
this.v,
|
||||
{ imageName: '' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'edit',
|
||||
'-o',
|
||||
`${this.page}:${this.h}:${this.v}:${outputFilePath}`,
|
||||
],
|
||||
this.compileDir,
|
||||
this.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', function () {
|
||||
expect(this.result).to.deep.equal({
|
||||
pdfPositions: this.records,
|
||||
downloadedFromCache: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a custom imageName', function () {
|
||||
const customImageName = 'foo/bar:tag-1'
|
||||
beforeEach(async function () {
|
||||
await this.CompileManager.promises.syncFromPdf(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.page,
|
||||
this.h,
|
||||
this.v,
|
||||
{ imageName: customImageName }
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute the synctex binary in a custom docker image', function () {
|
||||
const outputFilePath = `${this.compileDir}/output.pdf`
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
[
|
||||
'synctex',
|
||||
'edit',
|
||||
'-o',
|
||||
`${this.page}:${this.h}:${this.v}:${outputFilePath}`,
|
||||
],
|
||||
this.compileDir,
|
||||
customImageName,
|
||||
60000,
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('wordcount', function () {
|
||||
beforeEach(async function () {
|
||||
this.timeout = 60 * 1000
|
||||
this.filename = 'main.tex'
|
||||
this.image = 'example.com/image'
|
||||
|
||||
this.result = await this.CompileManager.promises.wordcount(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
this.filename,
|
||||
this.image
|
||||
)
|
||||
})
|
||||
|
||||
it('should run the texcount command', function () {
|
||||
this.filePath = `$COMPILE_DIR/${this.filename}`
|
||||
this.command = ['texcount', '-nocol', '-inc', this.filePath]
|
||||
|
||||
expect(this.CommandRunner.promises.run).to.have.been.calledWith(
|
||||
`${this.projectId}-${this.userId}`,
|
||||
this.command,
|
||||
this.compileDir,
|
||||
this.image,
|
||||
this.timeout,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the parsed output', function () {
|
||||
expect(this.result).to.deep.equal({
|
||||
encode: 'ascii',
|
||||
textWords: 2,
|
||||
headWords: 0,
|
||||
outside: 0,
|
||||
headers: 0,
|
||||
elements: 0,
|
||||
mathInline: 0,
|
||||
mathDisplay: 0,
|
||||
errors: 0,
|
||||
messages: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+12
-12
@@ -1,15 +1,15 @@
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
const { expect } = require('chai')
|
||||
import fs from 'node:fs'
|
||||
import Path from 'node:path'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
const MODULE_PATH = '../../../app/js/ContentCacheManager'
|
||||
|
||||
describe('ContentCacheManager', function () {
|
||||
let contentDir, pdfPath, xrefPath
|
||||
let ContentCacheManager, files, Settings
|
||||
before(function () {
|
||||
Settings = require('@overleaf/settings')
|
||||
ContentCacheManager = require(MODULE_PATH)
|
||||
beforeAll(async function () {
|
||||
Settings = (await import('@overleaf/settings')).default
|
||||
ContentCacheManager = (await import(MODULE_PATH)).default
|
||||
})
|
||||
let contentRanges, newContentRanges, reclaimed
|
||||
async function run(filePath, pdfSize, pdfCachingMinChunkSize) {
|
||||
@@ -35,7 +35,7 @@ describe('ContentCacheManager', function () {
|
||||
files[path] = await fs.promises.readFile(path)
|
||||
}
|
||||
}
|
||||
before(function () {
|
||||
beforeAll(function () {
|
||||
contentDir =
|
||||
'/overleaf/services/clsi/output/602cee6f6460fca0ba7921e6/content/1797a7f48f9-5abc1998509dea1f'
|
||||
pdfPath =
|
||||
@@ -47,7 +47,7 @@ describe('ContentCacheManager', function () {
|
||||
Settings.pdfCachingMinChunkSize = 1024
|
||||
})
|
||||
|
||||
before(async function () {
|
||||
beforeAll(async function () {
|
||||
await fs.promises.rm(contentDir, { recursive: true, force: true })
|
||||
await fs.promises.mkdir(contentDir, { recursive: true })
|
||||
await fs.promises.mkdir(Path.dirname(pdfPath), { recursive: true })
|
||||
@@ -66,7 +66,7 @@ describe('ContentCacheManager', function () {
|
||||
return Path.join('test/unit/js/snapshots/minimalCompile/chunks', hash)
|
||||
}
|
||||
let MINIMAL_SIZE, RANGE_1, RANGE_2, h1, h2, START_1, START_2, END_1, END_2
|
||||
before(async function () {
|
||||
beforeAll(async function () {
|
||||
await fs.promises.copyFile(PATH_MINIMAL_PDF, pdfPath)
|
||||
await fs.promises.copyFile(PATH_MINIMAL_XREF, xrefPath)
|
||||
const MINIMAL = await fs.promises.readFile(PATH_MINIMAL_PDF)
|
||||
@@ -85,7 +85,7 @@ describe('ContentCacheManager', function () {
|
||||
}
|
||||
|
||||
describe('with two ranges qualifying', function () {
|
||||
before(async function () {
|
||||
beforeAll(async function () {
|
||||
await runWithMinimal(500)
|
||||
})
|
||||
it('should produce two ranges', function () {
|
||||
@@ -133,7 +133,7 @@ describe('ContentCacheManager', function () {
|
||||
})
|
||||
|
||||
describe('when re-running with one range too small', function () {
|
||||
before(async function () {
|
||||
beforeAll(async function () {
|
||||
await runWithMinimal(1024)
|
||||
})
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('ContentCacheManager', function () {
|
||||
|
||||
describe('when re-running 5 more times', function () {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
before(async function () {
|
||||
beforeAll(async function () {
|
||||
await runWithMinimal(1024)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, beforeEach, it } from 'vitest'
|
||||
import path from 'node:path'
|
||||
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/ContentTypeMapper'
|
||||
)
|
||||
|
||||
describe('ContentTypeMapper', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
return (ctx.ContentTypeMapper = (await import(modulePath)).default)
|
||||
})
|
||||
|
||||
return describe('map', function () {
|
||||
it('should map .txt to text/plain', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.txt')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
it('should map .csv to text/csv', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.csv')
|
||||
return contentType.should.equal('text/csv')
|
||||
})
|
||||
|
||||
it('should map .pdf to application/pdf', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.pdf')
|
||||
return contentType.should.equal('application/pdf')
|
||||
})
|
||||
|
||||
it('should fall back to octet-stream', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.unknown')
|
||||
return contentType.should.equal('application/octet-stream')
|
||||
})
|
||||
|
||||
describe('coercing web files to plain text', function () {
|
||||
it('should map .js to plain text', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.js')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
it('should map .html to plain text', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.html')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
return it('should map .css to plain text', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.css')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
})
|
||||
|
||||
return describe('image files', function () {
|
||||
it('should map .png to image/png', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.png')
|
||||
return contentType.should.equal('image/png')
|
||||
})
|
||||
|
||||
it('should map .jpeg to image/jpeg', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.jpeg')
|
||||
return contentType.should.equal('image/jpeg')
|
||||
})
|
||||
|
||||
return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function (ctx) {
|
||||
const contentType = ctx.ContentTypeMapper.map('example.svg')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ContentTypeMapper'
|
||||
)
|
||||
|
||||
describe('ContentTypeMapper', function () {
|
||||
beforeEach(function () {
|
||||
return (this.ContentTypeMapper = SandboxedModule.require(modulePath))
|
||||
})
|
||||
|
||||
return describe('map', function () {
|
||||
it('should map .txt to text/plain', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.txt')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
it('should map .csv to text/csv', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.csv')
|
||||
return contentType.should.equal('text/csv')
|
||||
})
|
||||
|
||||
it('should map .pdf to application/pdf', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.pdf')
|
||||
return contentType.should.equal('application/pdf')
|
||||
})
|
||||
|
||||
it('should fall back to octet-stream', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.unknown')
|
||||
return contentType.should.equal('application/octet-stream')
|
||||
})
|
||||
|
||||
describe('coercing web files to plain text', function () {
|
||||
it('should map .js to plain text', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.js')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
it('should map .html to plain text', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.html')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
|
||||
return it('should map .css to plain text', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.css')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
})
|
||||
|
||||
return describe('image files', function () {
|
||||
it('should map .png to image/png', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.png')
|
||||
return contentType.should.equal('image/png')
|
||||
})
|
||||
|
||||
it('should map .jpeg to image/jpeg', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.jpeg')
|
||||
return contentType.should.equal('image/jpeg')
|
||||
})
|
||||
|
||||
return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function () {
|
||||
const contentType = this.ContentTypeMapper.map('example.svg')
|
||||
return contentType.should.equal('text/plain')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,259 @@
|
||||
import { vi, describe, beforeEach, it } from 'vitest'
|
||||
|
||||
import sinon from 'sinon'
|
||||
import path from 'node:path'
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/DockerLockManager'
|
||||
)
|
||||
|
||||
describe('DockerLockManager', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: (ctx.Settings = { clsi: { docker: {} } }),
|
||||
}))
|
||||
|
||||
return (ctx.LockManager = (await import(modulePath)).default)
|
||||
})
|
||||
|
||||
return describe('runWithLock', function () {
|
||||
describe('with a single lock', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.callback = sinon.stub()
|
||||
return ctx.LockManager.runWithLock(
|
||||
'lock-one',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
ctx.callback(err, ...Array.from(args))
|
||||
return resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
return it('should call the callback', function (ctx) {
|
||||
return ctx.callback
|
||||
.calledWith(null, 'hello', 'world')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with two locks', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.callback1 = sinon.stub()
|
||||
ctx.callback2 = sinon.stub()
|
||||
ctx.LockManager.runWithLock(
|
||||
'lock-one',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
return ctx.callback1(err, ...Array.from(args))
|
||||
}
|
||||
)
|
||||
return ctx.LockManager.runWithLock(
|
||||
'lock-two',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
|
||||
|
||||
(err, ...args) => {
|
||||
ctx.callback2(err, ...Array.from(args))
|
||||
return resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the first callback', function (ctx) {
|
||||
return ctx.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function (ctx) {
|
||||
return ctx.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with lock contention', function () {
|
||||
describe('where the first lock is released quickly', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.LockManager.MAX_LOCK_WAIT_TIME = 1000
|
||||
ctx.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
ctx.callback1 = sinon.stub()
|
||||
ctx.callback2 = sinon.stub()
|
||||
ctx.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'one'),
|
||||
100
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
return ctx.callback1(err, ...Array.from(args))
|
||||
}
|
||||
)
|
||||
return ctx.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'two'),
|
||||
200
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
ctx.callback2(err, ...Array.from(args))
|
||||
return resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the first callback', function (ctx) {
|
||||
return ctx.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function (ctx) {
|
||||
return ctx.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('where the first lock is held longer than the waiting time', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
let doneTwo
|
||||
ctx.LockManager.MAX_LOCK_HOLD_TIME = 10000
|
||||
ctx.LockManager.MAX_LOCK_WAIT_TIME = 1000
|
||||
ctx.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
ctx.callback1 = sinon.stub()
|
||||
ctx.callback2 = sinon.stub()
|
||||
let doneOne = (doneTwo = false)
|
||||
const finish = function (key) {
|
||||
if (key === 1) {
|
||||
doneOne = true
|
||||
}
|
||||
if (key === 2) {
|
||||
doneTwo = true
|
||||
}
|
||||
if (doneOne && doneTwo) {
|
||||
return resolve()
|
||||
}
|
||||
}
|
||||
ctx.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'one'),
|
||||
1100
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
ctx.callback1(err, ...Array.from(args))
|
||||
return finish(1)
|
||||
}
|
||||
)
|
||||
return ctx.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'two'),
|
||||
100
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
ctx.callback2(err, ...Array.from(args))
|
||||
return finish(2)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the first callback', function (ctx) {
|
||||
return ctx.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback with an error', function (ctx) {
|
||||
const error = sinon.match.instanceOf(Error)
|
||||
return ctx.callback2.calledWith(error).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('where the first lock is held longer than the max holding time', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
let doneTwo
|
||||
ctx.LockManager.MAX_LOCK_HOLD_TIME = 1000
|
||||
ctx.LockManager.MAX_LOCK_WAIT_TIME = 2000
|
||||
ctx.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
ctx.callback1 = sinon.stub()
|
||||
ctx.callback2 = sinon.stub()
|
||||
let doneOne = (doneTwo = false)
|
||||
const finish = function (key) {
|
||||
if (key === 1) {
|
||||
doneOne = true
|
||||
}
|
||||
if (key === 2) {
|
||||
doneTwo = true
|
||||
}
|
||||
if (doneOne && doneTwo) {
|
||||
return resolve()
|
||||
}
|
||||
}
|
||||
ctx.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'one'),
|
||||
1500
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
ctx.callback1(err, ...Array.from(args))
|
||||
return finish(1)
|
||||
}
|
||||
)
|
||||
return ctx.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'two'),
|
||||
100
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
ctx.callback2(err, ...Array.from(args))
|
||||
return finish(2)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the first callback', function (ctx) {
|
||||
return ctx.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function (ctx) {
|
||||
return ctx.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,246 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/DockerLockManager'
|
||||
)
|
||||
|
||||
describe('DockerLockManager', function () {
|
||||
beforeEach(function () {
|
||||
return (this.LockManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': (this.Settings = { clsi: { docker: {} } }),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
return describe('runWithLock', function () {
|
||||
describe('with a single lock', function () {
|
||||
beforeEach(function (done) {
|
||||
this.callback = sinon.stub()
|
||||
return this.LockManager.runWithLock(
|
||||
'lock-one',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback(err, ...Array.from(args))
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback
|
||||
.calledWith(null, 'hello', 'world')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with two locks', function () {
|
||||
beforeEach(function (done) {
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
this.LockManager.runWithLock(
|
||||
'lock-one',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
return this.callback1(err, ...Array.from(args))
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock-two',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function () {
|
||||
return this.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with lock contention', function () {
|
||||
describe('where the first lock is released quickly', function () {
|
||||
beforeEach(function (done) {
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 1000
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
return this.callback1(err, ...Array.from(args))
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function () {
|
||||
return this.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('where the first lock is held longer than the waiting time', function () {
|
||||
beforeEach(function (done) {
|
||||
let doneTwo
|
||||
this.LockManager.MAX_LOCK_HOLD_TIME = 10000
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 1000
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
let doneOne = (doneTwo = false)
|
||||
const finish = function (key) {
|
||||
if (key === 1) {
|
||||
doneOne = true
|
||||
}
|
||||
if (key === 2) {
|
||||
doneTwo = true
|
||||
}
|
||||
if (doneOne && doneTwo) {
|
||||
return done()
|
||||
}
|
||||
}
|
||||
this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'one'),
|
||||
1100
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback1(err, ...Array.from(args))
|
||||
return finish(1)
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return finish(2)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback with an error', function () {
|
||||
const error = sinon.match.instanceOf(Error)
|
||||
return this.callback2.calledWith(error).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('where the first lock is held longer than the max holding time', function () {
|
||||
beforeEach(function (done) {
|
||||
let doneTwo
|
||||
this.LockManager.MAX_LOCK_HOLD_TIME = 1000
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 2000
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100
|
||||
this.callback1 = sinon.stub()
|
||||
this.callback2 = sinon.stub()
|
||||
let doneOne = (doneTwo = false)
|
||||
const finish = function (key) {
|
||||
if (key === 1) {
|
||||
doneOne = true
|
||||
}
|
||||
if (key === 2) {
|
||||
doneTwo = true
|
||||
}
|
||||
if (doneOne && doneTwo) {
|
||||
return done()
|
||||
}
|
||||
}
|
||||
this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(
|
||||
() => releaseLock(null, 'hello', 'world', 'one'),
|
||||
1500
|
||||
),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback1(err, ...Array.from(args))
|
||||
return finish(1)
|
||||
}
|
||||
)
|
||||
return this.LockManager.runWithLock(
|
||||
'lock',
|
||||
releaseLock =>
|
||||
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 100),
|
||||
|
||||
(err, ...args) => {
|
||||
this.callback2(err, ...Array.from(args))
|
||||
return finish(2)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the first callback', function () {
|
||||
return this.callback1
|
||||
.calledWith(null, 'hello', 'world', 'one')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the second callback', function () {
|
||||
return this.callback2
|
||||
.calledWith(null, 'hello', 'world', 'two')
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
|
||||
import Path from 'node:path'
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import mockFs from 'mock-fs'
|
||||
|
||||
const MODULE_PATH = Path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/DraftModeManager'
|
||||
)
|
||||
|
||||
describe('DraftModeManager', () => {
|
||||
beforeEach(async ctx => {
|
||||
vi.doMock('node:fs/promises', () => ({
|
||||
default: fsPromises,
|
||||
}))
|
||||
|
||||
ctx.DraftModeManager = (await import(MODULE_PATH)).default
|
||||
ctx.filename = '/mock/filename.tex'
|
||||
ctx.contents = `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`
|
||||
mockFs({
|
||||
[ctx.filename]: ctx.contents,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
describe('injectDraftMode', () => {
|
||||
it('prepends a special command to the beginning of the file', async ctx => {
|
||||
await ctx.DraftModeManager.promises.injectDraftMode(ctx.filename)
|
||||
const contents = await fsPromises.readFile(ctx.filename, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
expect(contents).to.equal(
|
||||
'\\PassOptionsToPackage{draft}{graphicx}\\PassOptionsToPackage{draft}{graphics}' +
|
||||
ctx.contents
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,44 +0,0 @@
|
||||
const Path = require('node:path')
|
||||
const fsPromises = require('node:fs/promises')
|
||||
const { expect } = require('chai')
|
||||
const mockFs = require('mock-fs')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
|
||||
const MODULE_PATH = Path.join(__dirname, '../../../app/js/DraftModeManager')
|
||||
|
||||
describe('DraftModeManager', function () {
|
||||
beforeEach(function () {
|
||||
this.DraftModeManager = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'fs/promises': fsPromises,
|
||||
},
|
||||
})
|
||||
this.filename = '/mock/filename.tex'
|
||||
this.contents = `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`
|
||||
mockFs({
|
||||
[this.filename]: this.contents,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
describe('injectDraftMode', function () {
|
||||
it('prepends a special command to the beginning of the file', async function () {
|
||||
await this.DraftModeManager.promises.injectDraftMode(this.filename)
|
||||
const contents = await fsPromises.readFile(this.filename, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
expect(contents).to.equal(
|
||||
'\\PassOptionsToPackage{draft}{graphicx}\\PassOptionsToPackage{draft}{graphics}' +
|
||||
this.contents
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
+22
-20
@@ -1,32 +1,34 @@
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { expect } = require('chai')
|
||||
const { addLatexFdbMetrics } = require('../../../app/js/LatexMetrics')
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { expect, describe, beforeEach, it } from 'vitest'
|
||||
import LatexMetrics from '../../../app/js/LatexMetrics.js'
|
||||
|
||||
const { addLatexFdbMetrics } = LatexMetrics
|
||||
|
||||
describe('LatexMetrics', function () {
|
||||
describe('addLatexFdbMetrics', function () {
|
||||
beforeEach(function () {
|
||||
this.stats = {}
|
||||
Object.defineProperty(this.stats, 'latexmk', {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.stats = {}
|
||||
Object.defineProperty(ctx.stats, 'latexmk', {
|
||||
value: {},
|
||||
enumerable: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should do nothing if fdbContent is null or empty', function () {
|
||||
addLatexFdbMetrics(null, this.stats)
|
||||
expect(this.stats.latexmk).to.deep.equal({})
|
||||
addLatexFdbMetrics('', this.stats)
|
||||
expect(this.stats.latexmk).to.deep.equal({})
|
||||
it('should do nothing if fdbContent is null or empty', function (ctx) {
|
||||
addLatexFdbMetrics(null, ctx.stats)
|
||||
expect(ctx.stats.latexmk).to.deep.equal({})
|
||||
addLatexFdbMetrics('', ctx.stats)
|
||||
expect(ctx.stats.latexmk).to.deep.equal({})
|
||||
})
|
||||
|
||||
it('should parse v3 fdb content and add to stats', function () {
|
||||
it('should parse v3 fdb content and add to stats', function (ctx) {
|
||||
const fdbContent = fs.readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'v3.fdb_latexmk'),
|
||||
path.join(import.meta.dirname, 'fixtures', 'v3.fdb_latexmk'),
|
||||
'utf8'
|
||||
)
|
||||
addLatexFdbMetrics(fdbContent, this.stats)
|
||||
expect(this.stats.latexmk['fdb-file-types']).to.deep.equal({
|
||||
addLatexFdbMetrics(fdbContent, ctx.stats)
|
||||
expect(ctx.stats.latexmk['fdb-file-types']).to.deep.equal({
|
||||
system: [
|
||||
{ ext: 'fmt', count: 1, size: 3847283 },
|
||||
{ ext: 'map', count: 2, size: 1644257 },
|
||||
@@ -64,13 +66,13 @@ describe('LatexMetrics', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse v4 fdb content and add to stats', function () {
|
||||
it('should parse v4 fdb content and add to stats', function (ctx) {
|
||||
const fdbContent = fs.readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'v4.fdb_latexmk'),
|
||||
path.join(import.meta.dirname, 'fixtures', 'v4.fdb_latexmk'),
|
||||
'utf8'
|
||||
)
|
||||
addLatexFdbMetrics(fdbContent, this.stats)
|
||||
expect(this.stats.latexmk['fdb-file-types']).to.deep.equal({
|
||||
addLatexFdbMetrics(fdbContent, ctx.stats)
|
||||
expect(ctx.stats.latexmk['fdb-file-types']).to.deep.equal({
|
||||
system: [
|
||||
{ ext: 'fmt', count: 1, size: 8172536 },
|
||||
{ ext: 'map', count: 2, size: 4652176 },
|
||||
@@ -0,0 +1,335 @@
|
||||
import { vi, expect, describe, beforeEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const MODULE_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/LatexRunner'
|
||||
)
|
||||
|
||||
describe('LatexRunner', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.Settings = {
|
||||
docker: {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
},
|
||||
}
|
||||
ctx.commandRunnerOutput = {
|
||||
stdout: 'this is stdout',
|
||||
stderr: 'this is stderr',
|
||||
}
|
||||
ctx.CommandRunner = {
|
||||
run: sinon.stub().yields(null, ctx.commandRunnerOutput),
|
||||
}
|
||||
ctx.fs = {
|
||||
writeFile: sinon.stub().yields(),
|
||||
unlink: sinon
|
||||
.stub()
|
||||
.yields(new Error('ENOENT: no such file or directory, unlink ...')),
|
||||
}
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: ctx.Settings,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/CommandRunner', () => ({
|
||||
default: ctx.CommandRunner,
|
||||
}))
|
||||
|
||||
vi.doMock('fs', () => ({
|
||||
default: ctx.fs,
|
||||
}))
|
||||
|
||||
ctx.LatexRunner = (await import(MODULE_PATH)).default
|
||||
|
||||
ctx.directory = '/local/compile/directory'
|
||||
ctx.mainFile = 'main-file.tex'
|
||||
ctx.compiler = 'pdflatex'
|
||||
ctx.image = 'example.com/image'
|
||||
ctx.compileGroup = 'compile-group'
|
||||
ctx.callback = sinon.stub()
|
||||
ctx.project_id = 'project-id-123'
|
||||
ctx.env = { foo: '123' }
|
||||
ctx.timeout = 42000
|
||||
ctx.flags = []
|
||||
ctx.stopOnFirstError = false
|
||||
ctx.stats = {}
|
||||
ctx.timings = {}
|
||||
|
||||
ctx.call = function (callback) {
|
||||
this.LatexRunner.runLatex(
|
||||
this.project_id,
|
||||
{
|
||||
directory: this.directory,
|
||||
mainFile: this.mainFile,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
environment: this.env,
|
||||
compileGroup: this.compileGroup,
|
||||
flags: this.flags,
|
||||
stopOnFirstError: this.stopOnFirstError,
|
||||
timings: this.timings,
|
||||
stats: this.stats,
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('runLatex', () => {
|
||||
describe('normally', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should run the latex command', ctx => {
|
||||
ctx.CommandRunner.run.should.have.been.calledWith(
|
||||
ctx.project_id,
|
||||
[
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-time',
|
||||
'-f',
|
||||
'-pdf',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
],
|
||||
ctx.directory,
|
||||
ctx.image,
|
||||
ctx.timeout,
|
||||
ctx.env,
|
||||
ctx.compileGroup
|
||||
)
|
||||
})
|
||||
|
||||
it('should record the stdout and stderr', ctx => {
|
||||
ctx.fs.writeFile.should.have.been.calledWith(
|
||||
ctx.directory + '/' + 'output.stdout',
|
||||
'this is stdout',
|
||||
{ flag: 'wx' }
|
||||
)
|
||||
ctx.fs.writeFile.should.have.been.calledWith(
|
||||
ctx.directory + '/' + 'output.stderr',
|
||||
'this is stderr',
|
||||
{ flag: 'wx' }
|
||||
)
|
||||
ctx.fs.unlink.should.have.been.calledWith(
|
||||
ctx.directory + '/' + 'output.stdout'
|
||||
)
|
||||
ctx.fs.unlink.should.have.been.calledWith(
|
||||
ctx.directory + '/' + 'output.stderr'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not record cpu metrics', ctx => {
|
||||
expect(ctx.timings['cpu-percent']).to.not.exist
|
||||
expect(ctx.timings['cpu-time']).to.not.exist
|
||||
expect(ctx.timings['sys-time']).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a different compiler', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.compiler = 'lualatex'
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the appropriate latexmk flag', ctx => {
|
||||
ctx.CommandRunner.run.should.have.been.calledWith(ctx.project_id, [
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-time',
|
||||
'-f',
|
||||
'-lualatex',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with time -v', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.commandRunnerOutput.stderr =
|
||||
'\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' +
|
||||
'\tUser time (seconds): 0.28\n' +
|
||||
'\tSystem time (seconds): 0.70\n' +
|
||||
'\tPercent of CPU this job got: 98%\n'
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should record cpu metrics', ctx => {
|
||||
expect(ctx.timings['cpu-percent']).to.equal(98)
|
||||
expect(ctx.timings['cpu-time']).to.equal(0.28)
|
||||
expect(ctx.timings['sys-time']).to.equal(0.7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an .Rtex main file', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.mainFile = 'main-file.Rtex'
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should run the latex command on the equivalent .tex file', ctx => {
|
||||
const command = ctx.CommandRunner.run.args[0][1]
|
||||
const mainFile = command.slice(-1)[0]
|
||||
mainFile.should.equal('$COMPILE_DIR/main-file.tex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a flags option', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.flags = ['-shell-restricted', '-halt-on-error']
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should include the flags in the command', ctx => {
|
||||
const command = ctx.CommandRunner.run.args[0][1]
|
||||
const flags = command.filter(
|
||||
arg => arg === '-shell-restricted' || arg === '-halt-on-error'
|
||||
)
|
||||
flags.length.should.equal(2)
|
||||
flags[0].should.equal('-shell-restricted')
|
||||
flags[1].should.equal('-halt-on-error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the stopOnFirstError option', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.stopOnFirstError = true
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the appropriate flags', ctx => {
|
||||
ctx.CommandRunner.run.should.have.been.calledWith(ctx.project_id, [
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-time',
|
||||
'-halt-on-error',
|
||||
'-pdf',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with old latexmk timing output', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.commandRunnerOutput.stdout = fs.readFileSync(
|
||||
path.join(import.meta.dirname, 'fixtures', 'latexmk1.txt'),
|
||||
'utf-8'
|
||||
)
|
||||
// pass in the `latexmk` property to signal that we want to receive parsed stats
|
||||
ctx.stats.latexmk = {}
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse latexmk 4.52c (2017) timing information', ctx => {
|
||||
expect(ctx.stats.latexmk).to.deep.equal({
|
||||
'latexmk-rule-times': [
|
||||
{ rule: 'makeindex', time_ms: 30 },
|
||||
{ rule: 'bibtex', time_ms: 40 },
|
||||
{ rule: 'latex', time_ms: 690 },
|
||||
{ rule: 'makeindex', time_ms: 40 },
|
||||
{ rule: 'bibtex', time_ms: 39 },
|
||||
{ rule: 'latex', time_ms: 750 },
|
||||
{ rule: 'makeindex', time_ms: 39 },
|
||||
{ rule: 'bibtex', time_ms: 20 },
|
||||
{ rule: 'latex', time_ms: 770 },
|
||||
],
|
||||
'latexmk-rule-signature':
|
||||
'makeindex,bibtex,latex,makeindex,bibtex,latex,makeindex,bibtex,latex',
|
||||
'latexmk-rules-run': 9,
|
||||
'latexmk-time': { total: 2930 },
|
||||
'latexmk-img-times': [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with modern latexmk timing output', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.commandRunnerOutput.stdout = fs.readFileSync(
|
||||
path.join(import.meta.dirname, 'fixtures', 'latexmk2.txt'),
|
||||
'utf-8'
|
||||
)
|
||||
// pass in the `latexmk` property to signal that we want to receive parsed stats
|
||||
ctx.stats.latexmk = {}
|
||||
ctx.call(err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse latexmk 4.83 (2024) timing information', ctx => {
|
||||
expect(ctx.stats.latexmk).to.deep.equal({
|
||||
'latexmk-rule-times': [
|
||||
{ rule: 'latex', time_ms: 1880 },
|
||||
{ rule: 'makeindex', time_ms: 50 },
|
||||
{ rule: 'bibtex', time_ms: 50 },
|
||||
{ rule: 'latex', time_ms: 2180 },
|
||||
],
|
||||
'latexmk-rule-signature': 'latex,makeindex,bibtex,latex',
|
||||
'latexmk-time': {
|
||||
total: 4770,
|
||||
invoked: 4160,
|
||||
other: 610,
|
||||
},
|
||||
'latexmk-clock-time': 4870,
|
||||
'latexmk-rules-run': 4,
|
||||
'latexmk-img-times': [],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,289 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/LatexRunner'
|
||||
)
|
||||
|
||||
describe('LatexRunner', function () {
|
||||
beforeEach(function () {
|
||||
this.Settings = {
|
||||
docker: {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
},
|
||||
}
|
||||
this.commandRunnerOutput = {
|
||||
stdout: 'this is stdout',
|
||||
stderr: 'this is stderr',
|
||||
}
|
||||
this.CommandRunner = {
|
||||
run: sinon.stub().yields(null, this.commandRunnerOutput),
|
||||
}
|
||||
this.fs = {
|
||||
writeFile: sinon.stub().yields(),
|
||||
unlink: sinon
|
||||
.stub()
|
||||
.yields(new Error('ENOENT: no such file or directory, unlink ...')),
|
||||
}
|
||||
this.LatexRunner = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.Settings,
|
||||
'./CommandRunner': this.CommandRunner,
|
||||
fs: this.fs,
|
||||
},
|
||||
})
|
||||
|
||||
this.directory = '/local/compile/directory'
|
||||
this.mainFile = 'main-file.tex'
|
||||
this.compiler = 'pdflatex'
|
||||
this.image = 'example.com/image'
|
||||
this.compileGroup = 'compile-group'
|
||||
this.callback = sinon.stub()
|
||||
this.project_id = 'project-id-123'
|
||||
this.env = { foo: '123' }
|
||||
this.timeout = 42000
|
||||
this.flags = []
|
||||
this.stopOnFirstError = false
|
||||
this.stats = {}
|
||||
this.timings = {}
|
||||
|
||||
this.call = function (callback) {
|
||||
this.LatexRunner.runLatex(
|
||||
this.project_id,
|
||||
{
|
||||
directory: this.directory,
|
||||
mainFile: this.mainFile,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
environment: this.env,
|
||||
compileGroup: this.compileGroup,
|
||||
flags: this.flags,
|
||||
stopOnFirstError: this.stopOnFirstError,
|
||||
timings: this.timings,
|
||||
stats: this.stats,
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('runLatex', function () {
|
||||
describe('normally', function () {
|
||||
beforeEach(function (done) {
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should run the latex command', function () {
|
||||
this.CommandRunner.run.should.have.been.calledWith(
|
||||
this.project_id,
|
||||
[
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-time',
|
||||
'-f',
|
||||
'-pdf',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
],
|
||||
this.directory,
|
||||
this.image,
|
||||
this.timeout,
|
||||
this.env,
|
||||
this.compileGroup
|
||||
)
|
||||
})
|
||||
|
||||
it('should record the stdout and stderr', function () {
|
||||
this.fs.writeFile.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stdout',
|
||||
'this is stdout',
|
||||
{ flag: 'wx' }
|
||||
)
|
||||
this.fs.writeFile.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stderr',
|
||||
'this is stderr',
|
||||
{ flag: 'wx' }
|
||||
)
|
||||
this.fs.unlink.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stdout'
|
||||
)
|
||||
this.fs.unlink.should.have.been.calledWith(
|
||||
this.directory + '/' + 'output.stderr'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not record cpu metrics', function () {
|
||||
expect(this.timings['cpu-percent']).to.not.exist
|
||||
expect(this.timings['cpu-time']).to.not.exist
|
||||
expect(this.timings['sys-time']).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a different compiler', function () {
|
||||
beforeEach(function (done) {
|
||||
this.compiler = 'lualatex'
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should set the appropriate latexmk flag', function () {
|
||||
this.CommandRunner.run.should.have.been.calledWith(this.project_id, [
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-time',
|
||||
'-f',
|
||||
'-lualatex',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with time -v', function () {
|
||||
beforeEach(function (done) {
|
||||
this.commandRunnerOutput.stderr =
|
||||
'\tCommand being timed: "sh -c timeout 1 yes > /dev/null"\n' +
|
||||
'\tUser time (seconds): 0.28\n' +
|
||||
'\tSystem time (seconds): 0.70\n' +
|
||||
'\tPercent of CPU this job got: 98%\n'
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should record cpu metrics', function () {
|
||||
expect(this.timings['cpu-percent']).to.equal(98)
|
||||
expect(this.timings['cpu-time']).to.equal(0.28)
|
||||
expect(this.timings['sys-time']).to.equal(0.7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an .Rtex main file', function () {
|
||||
beforeEach(function (done) {
|
||||
this.mainFile = 'main-file.Rtex'
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should run the latex command on the equivalent .tex file', function () {
|
||||
const command = this.CommandRunner.run.args[0][1]
|
||||
const mainFile = command.slice(-1)[0]
|
||||
mainFile.should.equal('$COMPILE_DIR/main-file.tex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a flags option', function () {
|
||||
beforeEach(function (done) {
|
||||
this.flags = ['-shell-restricted', '-halt-on-error']
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should include the flags in the command', function () {
|
||||
const command = this.CommandRunner.run.args[0][1]
|
||||
const flags = command.filter(
|
||||
arg => arg === '-shell-restricted' || arg === '-halt-on-error'
|
||||
)
|
||||
flags.length.should.equal(2)
|
||||
flags[0].should.equal('-shell-restricted')
|
||||
flags[1].should.equal('-halt-on-error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the stopOnFirstError option', function () {
|
||||
beforeEach(function (done) {
|
||||
this.stopOnFirstError = true
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should set the appropriate flags', function () {
|
||||
this.CommandRunner.run.should.have.been.calledWith(this.project_id, [
|
||||
'latexmk',
|
||||
'-cd',
|
||||
'-jobname=output',
|
||||
'-auxdir=$COMPILE_DIR',
|
||||
'-outdir=$COMPILE_DIR',
|
||||
'-synctex=1',
|
||||
'-interaction=batchmode',
|
||||
'-time',
|
||||
'-halt-on-error',
|
||||
'-pdf',
|
||||
'$COMPILE_DIR/main-file.tex',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with old latexmk timing output', function () {
|
||||
beforeEach(function (done) {
|
||||
this.commandRunnerOutput.stdout = fs.readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'latexmk1.txt'),
|
||||
'utf-8'
|
||||
)
|
||||
// pass in the `latexmk` property to signal that we want to receive parsed stats
|
||||
this.stats.latexmk = {}
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should parse latexmk 4.52c (2017) timing information', function () {
|
||||
expect(this.stats.latexmk).to.deep.equal({
|
||||
'latexmk-rule-times': [
|
||||
{ rule: 'makeindex', time_ms: 30 },
|
||||
{ rule: 'bibtex', time_ms: 40 },
|
||||
{ rule: 'latex', time_ms: 690 },
|
||||
{ rule: 'makeindex', time_ms: 40 },
|
||||
{ rule: 'bibtex', time_ms: 39 },
|
||||
{ rule: 'latex', time_ms: 750 },
|
||||
{ rule: 'makeindex', time_ms: 39 },
|
||||
{ rule: 'bibtex', time_ms: 20 },
|
||||
{ rule: 'latex', time_ms: 770 },
|
||||
],
|
||||
'latexmk-rule-signature':
|
||||
'makeindex,bibtex,latex,makeindex,bibtex,latex,makeindex,bibtex,latex',
|
||||
'latexmk-rules-run': 9,
|
||||
'latexmk-time': { total: 2930 },
|
||||
'latexmk-img-times': [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with modern latexmk timing output', function () {
|
||||
beforeEach(function (done) {
|
||||
this.commandRunnerOutput.stdout = fs.readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'latexmk2.txt'),
|
||||
'utf-8'
|
||||
)
|
||||
// pass in the `latexmk` property to signal that we want to receive parsed stats
|
||||
this.stats.latexmk = {}
|
||||
this.call(done)
|
||||
})
|
||||
|
||||
it('should parse latexmk 4.83 (2024) timing information', function () {
|
||||
expect(this.stats.latexmk).to.deep.equal({
|
||||
'latexmk-rule-times': [
|
||||
{ rule: 'latex', time_ms: 1880 },
|
||||
{ rule: 'makeindex', time_ms: 50 },
|
||||
{ rule: 'bibtex', time_ms: 50 },
|
||||
{ rule: 'latex', time_ms: 2180 },
|
||||
],
|
||||
'latexmk-rule-signature': 'latex,makeindex,bibtex,latex',
|
||||
'latexmk-time': {
|
||||
total: 4770,
|
||||
invoked: 4160,
|
||||
other: 610,
|
||||
},
|
||||
'latexmk-clock-time': 4870,
|
||||
'latexmk-rules-run': 4,
|
||||
'latexmk-img-times': [],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import * as Errors from '../../../app/js/Errors.js'
|
||||
import path from 'node:path'
|
||||
|
||||
const modulePath = path.join(import.meta.dirname, '../../../app/js/LockManager')
|
||||
|
||||
describe('LockManager', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.key = '/local/compile/directory'
|
||||
ctx.clock = sinon.useFakeTimers()
|
||||
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: (ctx.Metrics = {
|
||||
inc: sinon.stub(),
|
||||
gauge: sinon.stub(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: (ctx.Settings = {
|
||||
compileConcurrencyLimit: 5,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/Errors', () => ({
|
||||
default: (ctx.Erros = Errors),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/RequestParser', () => ({
|
||||
default: { MAX_TIMEOUT: 600 },
|
||||
}))
|
||||
|
||||
ctx.LockManager = (await import(modulePath)).default
|
||||
})
|
||||
|
||||
afterEach(ctx => {
|
||||
ctx.clock.restore()
|
||||
})
|
||||
|
||||
describe('when the lock is available', () => {
|
||||
it('the lock can be acquired', ctx => {
|
||||
const lock = ctx.LockManager.acquire(ctx.key)
|
||||
expect(lock).to.exist
|
||||
lock.release()
|
||||
})
|
||||
})
|
||||
|
||||
describe('after the lock is acquired', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.lock = ctx.LockManager.acquire(ctx.key)
|
||||
})
|
||||
|
||||
afterEach(ctx => {
|
||||
if (ctx.lock != null) {
|
||||
ctx.lock.release()
|
||||
}
|
||||
})
|
||||
|
||||
it("the lock can't be acquired again", ctx => {
|
||||
expect(() => ctx.LockManager.acquire(ctx.key)).to.throw(
|
||||
Errors.AlreadyCompilingError
|
||||
)
|
||||
})
|
||||
|
||||
it('another lock can be acquired', ctx => {
|
||||
const lock = ctx.LockManager.acquire('another key')
|
||||
expect(lock).to.exist
|
||||
lock.release()
|
||||
})
|
||||
|
||||
it('the lock can be acquired again after an expiry period', ctx => {
|
||||
// The expiry time is a little bit over 10 minutes. Let's wait 15 minutes.
|
||||
ctx.clock.tick(15 * 60 * 1000)
|
||||
ctx.lock = ctx.LockManager.acquire(ctx.key)
|
||||
expect(ctx.lock).to.exist
|
||||
})
|
||||
|
||||
it('the lock can be acquired again after it was released', ctx => {
|
||||
ctx.lock.release()
|
||||
ctx.lock = ctx.LockManager.acquire(ctx.key)
|
||||
expect(ctx.lock).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('concurrency limit', () => {
|
||||
it('exceeding the limit', ctx => {
|
||||
for (let i = 0; i <= ctx.Settings.compileConcurrencyLimit; i++) {
|
||||
ctx.LockManager.acquire('test_key' + i)
|
||||
}
|
||||
ctx.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(false)
|
||||
expect(() =>
|
||||
ctx.LockManager.acquire(
|
||||
'test_key_' + (ctx.Settings.compileConcurrencyLimit + 1),
|
||||
false
|
||||
)
|
||||
).to.throw(Errors.TooManyCompileRequestsError)
|
||||
|
||||
ctx.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('within the limit', ctx => {
|
||||
for (let i = 0; i <= ctx.Settings.compileConcurrencyLimit - 1; i++) {
|
||||
ctx.LockManager.acquire('test_key' + i)
|
||||
}
|
||||
ctx.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(false)
|
||||
|
||||
const lock = ctx.LockManager.acquire(
|
||||
'test_key_' + ctx.Settings.compileConcurrencyLimit,
|
||||
false
|
||||
)
|
||||
|
||||
expect(lock.key).to.equal(
|
||||
'test_key_' + ctx.Settings.compileConcurrencyLimit
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,116 +0,0 @@
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/LockManager'
|
||||
)
|
||||
const Errors = require('../../../app/js/Errors')
|
||||
|
||||
describe('LockManager', function () {
|
||||
beforeEach(function () {
|
||||
this.key = '/local/compile/directory'
|
||||
this.clock = sinon.useFakeTimers()
|
||||
this.LockManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/metrics': (this.Metrics = {
|
||||
inc: sinon.stub(),
|
||||
gauge: sinon.stub(),
|
||||
}),
|
||||
'@overleaf/settings': (this.Settings = {
|
||||
compileConcurrencyLimit: 5,
|
||||
}),
|
||||
'./Errors': (this.Erros = Errors),
|
||||
'./RequestParser': { MAX_TIMEOUT: 600 },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.clock.restore()
|
||||
})
|
||||
|
||||
describe('when the lock is available', function () {
|
||||
it('the lock can be acquired', function () {
|
||||
const lock = this.LockManager.acquire(this.key)
|
||||
expect(lock).to.exist
|
||||
lock.release()
|
||||
})
|
||||
})
|
||||
|
||||
describe('after the lock is acquired', function () {
|
||||
beforeEach(function () {
|
||||
this.lock = this.LockManager.acquire(this.key)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
if (this.lock != null) {
|
||||
this.lock.release()
|
||||
}
|
||||
})
|
||||
|
||||
it("the lock can't be acquired again", function () {
|
||||
expect(() => this.LockManager.acquire(this.key)).to.throw(
|
||||
Errors.AlreadyCompilingError
|
||||
)
|
||||
})
|
||||
|
||||
it('another lock can be acquired', function () {
|
||||
const lock = this.LockManager.acquire('another key')
|
||||
expect(lock).to.exist
|
||||
lock.release()
|
||||
})
|
||||
|
||||
it('the lock can be acquired again after an expiry period', function () {
|
||||
// The expiry time is a little bit over 10 minutes. Let's wait 15 minutes.
|
||||
this.clock.tick(15 * 60 * 1000)
|
||||
this.lock = this.LockManager.acquire(this.key)
|
||||
expect(this.lock).to.exist
|
||||
})
|
||||
|
||||
it('the lock can be acquired again after it was released', function () {
|
||||
this.lock.release()
|
||||
this.lock = this.LockManager.acquire(this.key)
|
||||
expect(this.lock).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('concurrency limit', function () {
|
||||
it('exceeding the limit', function () {
|
||||
for (let i = 0; i <= this.Settings.compileConcurrencyLimit; i++) {
|
||||
this.LockManager.acquire('test_key' + i)
|
||||
}
|
||||
this.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(false)
|
||||
expect(() =>
|
||||
this.LockManager.acquire(
|
||||
'test_key_' + (this.Settings.compileConcurrencyLimit + 1),
|
||||
false
|
||||
)
|
||||
).to.throw(Errors.TooManyCompileRequestsError)
|
||||
|
||||
this.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('within the limit', function () {
|
||||
for (let i = 0; i <= this.Settings.compileConcurrencyLimit - 1; i++) {
|
||||
this.LockManager.acquire('test_key' + i)
|
||||
}
|
||||
this.Metrics.inc
|
||||
.calledWith('exceeded-compilier-concurrency-limit')
|
||||
.should.equal(false)
|
||||
|
||||
const lock = this.LockManager.acquire(
|
||||
'test_key_' + this.Settings.compileConcurrencyLimit,
|
||||
false
|
||||
)
|
||||
|
||||
expect(lock.key).to.equal(
|
||||
'test_key_' + this.Settings.compileConcurrencyLimit
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { vi, describe, beforeEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import path from 'node:path'
|
||||
const MODULE_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/OutputController'
|
||||
)
|
||||
|
||||
describe('OutputController', () => {
|
||||
describe('createOutputZip', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.archive = {}
|
||||
|
||||
ctx.pipeline = sinon.stub().resolves()
|
||||
|
||||
ctx.archiveFilesForBuild = sinon.stub().resolves(ctx.archive)
|
||||
|
||||
vi.doMock('../../../app/js/OutputFileArchiveManager', () => ({
|
||||
default: {
|
||||
archiveFilesForBuild: ctx.archiveFilesForBuild,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('node:stream/promises', () => ({
|
||||
pipeline: ctx.pipeline,
|
||||
}))
|
||||
|
||||
ctx.OutputController = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
describe('when OutputFileArchiveManager creates an archive', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res = {
|
||||
attachment: sinon.stub(),
|
||||
setHeader: sinon.stub(),
|
||||
}
|
||||
ctx.req = {
|
||||
params: {
|
||||
project_id: 'project-id-123',
|
||||
user_id: 'user-id-123',
|
||||
build_id: 'build-id-123',
|
||||
},
|
||||
query: {
|
||||
files: ['output.tex'],
|
||||
},
|
||||
}
|
||||
ctx.pipeline.callsFake(() => {
|
||||
resolve()
|
||||
return Promise.resolve()
|
||||
})
|
||||
ctx.OutputController.createOutputZip(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a pipeline from the archive to the response', ctx => {
|
||||
sinon.assert.calledWith(ctx.pipeline, ctx.archive, ctx.res)
|
||||
})
|
||||
|
||||
it('calls the express convenience method to set attachment headers', ctx => {
|
||||
sinon.assert.calledWith(ctx.res.attachment, 'output.zip')
|
||||
})
|
||||
|
||||
it('sets the X-Content-Type-Options header to nosniff', ctx => {
|
||||
sinon.assert.calledWith(
|
||||
ctx.res.setHeader,
|
||||
'X-Content-Type-Options',
|
||||
'nosniff'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when OutputFileArchiveManager throws an error', () => {
|
||||
let error
|
||||
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
error = new Error('error message')
|
||||
|
||||
ctx.archiveFilesForBuild.rejects(error)
|
||||
|
||||
ctx.res = {
|
||||
status: sinon.stub().returnsThis(),
|
||||
send: sinon.stub(),
|
||||
}
|
||||
ctx.req = {
|
||||
params: {
|
||||
project_id: 'project-id-123',
|
||||
user_id: 'user-id-123',
|
||||
build_id: 'build-id-123',
|
||||
},
|
||||
query: {
|
||||
files: ['output.tex'],
|
||||
},
|
||||
}
|
||||
ctx.OutputController.createOutputZip(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
(ctx.next = sinon.stub().callsFake(() => {
|
||||
resolve()
|
||||
}))
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('calls next with the error', ctx => {
|
||||
sinon.assert.calledWith(ctx.next, error)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,105 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputController'
|
||||
)
|
||||
|
||||
describe('OutputController', function () {
|
||||
describe('createOutputZip', function () {
|
||||
beforeEach(function () {
|
||||
this.archive = {}
|
||||
|
||||
this.pipeline = sinon.stub().resolves()
|
||||
|
||||
this.archiveFilesForBuild = sinon.stub().resolves(this.archive)
|
||||
|
||||
this.OutputController = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'./OutputFileArchiveManager': {
|
||||
archiveFilesForBuild: this.archiveFilesForBuild,
|
||||
},
|
||||
'stream/promises': {
|
||||
pipeline: this.pipeline,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('when OutputFileArchiveManager creates an archive', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res = {
|
||||
attachment: sinon.stub(),
|
||||
setHeader: sinon.stub(),
|
||||
}
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: 'project-id-123',
|
||||
user_id: 'user-id-123',
|
||||
build_id: 'build-id-123',
|
||||
},
|
||||
query: {
|
||||
files: ['output.tex'],
|
||||
},
|
||||
}
|
||||
this.pipeline.callsFake(() => {
|
||||
done()
|
||||
return Promise.resolve()
|
||||
})
|
||||
this.OutputController.createOutputZip(this.req, this.res)
|
||||
})
|
||||
|
||||
it('creates a pipeline from the archive to the response', function () {
|
||||
sinon.assert.calledWith(this.pipeline, this.archive, this.res)
|
||||
})
|
||||
|
||||
it('calls the express convenience method to set attachment headers', function () {
|
||||
sinon.assert.calledWith(this.res.attachment, 'output.zip')
|
||||
})
|
||||
|
||||
it('sets the X-Content-Type-Options header to nosniff', function () {
|
||||
sinon.assert.calledWith(
|
||||
this.res.setHeader,
|
||||
'X-Content-Type-Options',
|
||||
'nosniff'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when OutputFileArchiveManager throws an error', function () {
|
||||
let error
|
||||
|
||||
beforeEach(function (done) {
|
||||
error = new Error('error message')
|
||||
|
||||
this.archiveFilesForBuild.rejects(error)
|
||||
|
||||
this.res = {
|
||||
status: sinon.stub().returnsThis(),
|
||||
send: sinon.stub(),
|
||||
}
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: 'project-id-123',
|
||||
user_id: 'user-id-123',
|
||||
build_id: 'build-id-123',
|
||||
},
|
||||
query: {
|
||||
files: ['output.tex'],
|
||||
},
|
||||
}
|
||||
this.OutputController.createOutputZip(
|
||||
this.req,
|
||||
this.res,
|
||||
(this.next = sinon.stub().callsFake(() => {
|
||||
done()
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
it('calls next with the error', function () {
|
||||
sinon.assert.calledWith(this.next, error)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,238 @@
|
||||
import { vi, assert, expect, describe, afterEach, beforeEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import path from 'node:path'
|
||||
|
||||
const MODULE_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/OutputFileArchiveManager'
|
||||
)
|
||||
|
||||
describe('OutputFileArchiveManager', () => {
|
||||
const userId = 'user-id-123'
|
||||
const projectId = 'project-id-123'
|
||||
const buildId = 'build-id-123'
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
beforeEach(async ctx => {
|
||||
ctx.OutputFileFinder = {
|
||||
promises: {
|
||||
findOutputFiles: sinon.stub().resolves({ outputFiles: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
ctx.OutputCacheManger = {
|
||||
path: sinon.stub().callsFake((build, path) => {
|
||||
return `${build}/${path}`
|
||||
}),
|
||||
}
|
||||
|
||||
ctx.archive = {
|
||||
append: sinon.stub(),
|
||||
finalize: sinon.stub().resolves(),
|
||||
on: sinon.stub(),
|
||||
}
|
||||
|
||||
ctx.archiver = sinon.stub().returns(ctx.archive)
|
||||
|
||||
ctx.outputDir = '/output/dir'
|
||||
|
||||
ctx.fs = {
|
||||
open: sinon.stub().callsFake(file => ({
|
||||
createReadStream: sinon.stub().returns(`handle: ${file}`),
|
||||
})),
|
||||
}
|
||||
|
||||
vi.doMock('../../../app/js/OutputFileFinder', () => ({
|
||||
default: ctx.OutputFileFinder,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/OutputCacheManager', () => ({
|
||||
default: ctx.OutputCacheManger,
|
||||
}))
|
||||
|
||||
vi.doMock('archiver', () => ({
|
||||
default: ctx.archiver,
|
||||
}))
|
||||
|
||||
vi.doMock('node:fs/promises', () => ctx.fs)
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: {
|
||||
path: {
|
||||
outputDir: ctx.outputDir,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
ctx.OutputFileArchiveManager = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
describe('when the output cache directory contains only exportable files', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
],
|
||||
})
|
||||
await ctx.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('creates a zip archive', ctx => {
|
||||
sinon.assert.calledWith(ctx.archiver, 'zip')
|
||||
})
|
||||
|
||||
it('listens to errors from the archive', ctx => {
|
||||
sinon.assert.calledWith(ctx.archive.on, 'error', sinon.match.func)
|
||||
})
|
||||
|
||||
it('adds all the output files to the archive', ctx => {
|
||||
expect(ctx.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
|
||||
it('finalizes the archive after all files are appended', ctx => {
|
||||
sinon.assert.called(ctx.archive.finalize)
|
||||
expect(ctx.archive.finalize.calledBefore(ctx.archive.append)).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the directory includes files ignored by web', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
{ path: 'output.pdf' },
|
||||
],
|
||||
})
|
||||
await ctx.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('only includes the non-ignored files in the archive', ctx => {
|
||||
expect(ctx.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when one of the files is called output.pdf', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
{ path: 'output.pdf' },
|
||||
],
|
||||
})
|
||||
await ctx.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('does not include that file in the archive', ctx => {
|
||||
expect(ctx.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
ctx.archive.append,
|
||||
`handle: ${ctx.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the output directory cannot be accessed', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.OutputFileFinder.promises.findOutputFiles.rejects({
|
||||
code: 'ENOENT',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects with a NotFoundError', async ctx => {
|
||||
try {
|
||||
await ctx.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
assert.fail('should have thrown a NotFoundError')
|
||||
} catch (err) {
|
||||
expect(err).to.haveOwnProperty('name', 'NotFoundError')
|
||||
}
|
||||
})
|
||||
|
||||
it('does not create an archive', ctx => {
|
||||
expect(ctx.archiver.called).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,229 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { assert, expect } = require('chai')
|
||||
|
||||
const MODULE_PATH = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputFileArchiveManager'
|
||||
)
|
||||
|
||||
describe('OutputFileArchiveManager', function () {
|
||||
const userId = 'user-id-123'
|
||||
const projectId = 'project-id-123'
|
||||
const buildId = 'build-id-123'
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
this.OutputFileFinder = {
|
||||
promises: {
|
||||
findOutputFiles: sinon.stub().resolves({ outputFiles: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
this.OutputCacheManger = {
|
||||
path: sinon.stub().callsFake((build, path) => {
|
||||
return `${build}/${path}`
|
||||
}),
|
||||
}
|
||||
|
||||
this.archive = {
|
||||
append: sinon.stub(),
|
||||
finalize: sinon.stub().resolves(),
|
||||
on: sinon.stub(),
|
||||
}
|
||||
|
||||
this.archiver = sinon.stub().returns(this.archive)
|
||||
|
||||
this.outputDir = '/output/dir'
|
||||
|
||||
this.fs = {
|
||||
open: sinon.stub().callsFake(file => ({
|
||||
createReadStream: sinon.stub().returns(`handle: ${file}`),
|
||||
})),
|
||||
}
|
||||
|
||||
this.OutputFileArchiveManager = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'./OutputFileFinder': this.OutputFileFinder,
|
||||
'./OutputCacheManager': this.OutputCacheManger,
|
||||
archiver: this.archiver,
|
||||
'fs/promises': this.fs,
|
||||
'@overleaf/settings': {
|
||||
path: {
|
||||
outputDir: this.outputDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the output cache directory contains only exportable files', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
],
|
||||
})
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('creates a zip archive', function () {
|
||||
sinon.assert.calledWith(this.archiver, 'zip')
|
||||
})
|
||||
|
||||
it('listens to errors from the archive', function () {
|
||||
sinon.assert.calledWith(this.archive.on, 'error', sinon.match.func)
|
||||
})
|
||||
|
||||
it('adds all the output files to the archive', function () {
|
||||
expect(this.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
|
||||
it('finalizes the archive after all files are appended', function () {
|
||||
sinon.assert.called(this.archive.finalize)
|
||||
expect(this.archive.finalize.calledBefore(this.archive.append)).to.be
|
||||
.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the directory includes files ignored by web', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
{ path: 'output.pdf' },
|
||||
],
|
||||
})
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('only includes the non-ignored files in the archive', function () {
|
||||
expect(this.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when one of the files is called output.pdf', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.resolves({
|
||||
outputFiles: [
|
||||
{ path: 'file_1' },
|
||||
{ path: 'file_2' },
|
||||
{ path: 'file_3' },
|
||||
{ path: 'file_4' },
|
||||
{ path: 'output.pdf' },
|
||||
],
|
||||
})
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
})
|
||||
|
||||
it('does not include that file in the archive', function () {
|
||||
expect(this.archive.append.callCount).to.equal(4)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_1`,
|
||||
sinon.match({ name: 'file_1' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_2`,
|
||||
sinon.match({ name: 'file_2' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_3`,
|
||||
sinon.match({ name: 'file_3' })
|
||||
)
|
||||
sinon.assert.calledWith(
|
||||
this.archive.append,
|
||||
`handle: ${this.outputDir}/${projectId}-${userId}/${buildId}/file_4`,
|
||||
sinon.match({ name: 'file_4' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the output directory cannot be accessed', function () {
|
||||
beforeEach(async function () {
|
||||
this.OutputFileFinder.promises.findOutputFiles.rejects({
|
||||
code: 'ENOENT',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects with a NotFoundError', async function () {
|
||||
try {
|
||||
await this.OutputFileArchiveManager.archiveFilesForBuild(
|
||||
projectId,
|
||||
userId,
|
||||
buildId
|
||||
)
|
||||
assert.fail('should have thrown a NotFoundError')
|
||||
} catch (err) {
|
||||
expect(err).to.haveOwnProperty('name', 'NotFoundError')
|
||||
}
|
||||
})
|
||||
|
||||
it('does not create an archive', function () {
|
||||
expect(this.archiver.called).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import sinon from 'sinon'
|
||||
import { expect, describe, beforeEach, afterEach, it } from 'vitest'
|
||||
import mockFs from 'mock-fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/OutputFileFinder'
|
||||
)
|
||||
|
||||
describe('OutputFileFinder', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.OutputFileFinder = (await import(modulePath)).default
|
||||
ctx.directory = '/test/dir'
|
||||
ctx.callback = sinon.stub()
|
||||
|
||||
mockFs({
|
||||
[ctx.directory]: {
|
||||
resource: {
|
||||
'path.tex': 'a source file',
|
||||
},
|
||||
'output.pdf': 'a generated pdf file',
|
||||
extra: {
|
||||
'file.tex': 'a generated tex file',
|
||||
},
|
||||
'sneaky-file': mockFs.symlink({
|
||||
path: '../foo',
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
describe('findOutputFiles', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.resource_path = 'resource/path.tex'
|
||||
ctx.output_paths = ['output.pdf', 'extra/file.tex']
|
||||
ctx.all_paths = ctx.output_paths.concat([ctx.resource_path])
|
||||
ctx.resources = [{ path: (ctx.resource_path = 'resource/path.tex') }]
|
||||
const { outputFiles, allEntries } =
|
||||
await ctx.OutputFileFinder.promises.findOutputFiles(
|
||||
ctx.resources,
|
||||
ctx.directory
|
||||
)
|
||||
ctx.outputFiles = outputFiles
|
||||
ctx.allEntries = allEntries
|
||||
})
|
||||
|
||||
it('should only return the output files, not directories or resource paths', function (ctx) {
|
||||
expect(ctx.outputFiles).to.have.deep.members([
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'extra/file.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
])
|
||||
expect(ctx.allEntries).to.deep.equal([
|
||||
'extra/file.tex',
|
||||
'extra/',
|
||||
'output.pdf',
|
||||
'resource/path.tex',
|
||||
'resource/',
|
||||
'sneaky-file',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,72 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputFileFinder'
|
||||
)
|
||||
const { expect } = require('chai')
|
||||
const mockFs = require('mock-fs')
|
||||
|
||||
describe('OutputFileFinder', function () {
|
||||
beforeEach(function () {
|
||||
this.OutputFileFinder = SandboxedModule.require(modulePath, {})
|
||||
this.directory = '/test/dir'
|
||||
this.callback = sinon.stub()
|
||||
|
||||
mockFs({
|
||||
[this.directory]: {
|
||||
resource: {
|
||||
'path.tex': 'a source file',
|
||||
},
|
||||
'output.pdf': 'a generated pdf file',
|
||||
extra: {
|
||||
'file.tex': 'a generated tex file',
|
||||
},
|
||||
'sneaky-file': mockFs.symlink({
|
||||
path: '../foo',
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
describe('findOutputFiles', function () {
|
||||
beforeEach(async function () {
|
||||
this.resource_path = 'resource/path.tex'
|
||||
this.output_paths = ['output.pdf', 'extra/file.tex']
|
||||
this.all_paths = this.output_paths.concat([this.resource_path])
|
||||
this.resources = [{ path: (this.resource_path = 'resource/path.tex') }]
|
||||
const { outputFiles, allEntries } =
|
||||
await this.OutputFileFinder.promises.findOutputFiles(
|
||||
this.resources,
|
||||
this.directory
|
||||
)
|
||||
this.outputFiles = outputFiles
|
||||
this.allEntries = allEntries
|
||||
})
|
||||
|
||||
it('should only return the output files, not directories or resource paths', function () {
|
||||
expect(this.outputFiles).to.have.deep.members([
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'extra/file.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
])
|
||||
expect(this.allEntries).to.deep.equal([
|
||||
'extra/file.tex',
|
||||
'extra/',
|
||||
'output.pdf',
|
||||
'resource/path.tex',
|
||||
'resource/',
|
||||
'sneaky-file',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,193 @@
|
||||
import { vi, describe, beforeEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import path from 'node:path'
|
||||
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/OutputFileOptimiser'
|
||||
)
|
||||
|
||||
describe('OutputFileOptimiser', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
vi.doMock('fs', () => ({
|
||||
default: (ctx.fs = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('path', () => ({
|
||||
default: (ctx.Path = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('child_process', () => ({
|
||||
default: { spawn: (ctx.spawn = sinon.stub()) },
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: {},
|
||||
}))
|
||||
|
||||
ctx.OutputFileOptimiser = (await import(modulePath)).default
|
||||
ctx.directory = '/test/dir'
|
||||
return (ctx.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('optimiseFile', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.src = './output.pdf'
|
||||
return (ctx.dst = './output.pdf')
|
||||
})
|
||||
|
||||
describe('when the file is not a pdf file', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.src = './output.log'
|
||||
ctx.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, false)
|
||||
ctx.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
ctx.OutputFileOptimiser.optimiseFile(ctx.src, ctx.dst, err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not check if the file is optimised', function (ctx) {
|
||||
return ctx.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(ctx.src)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should not optimise the file', function (ctx) {
|
||||
return ctx.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(ctx.src, ctx.dst)
|
||||
.should.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the pdf file is not optimised', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, false)
|
||||
ctx.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
ctx.OutputFileOptimiser.optimiseFile(ctx.src, ctx.dst, err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should check if the pdf is optimised', function (ctx) {
|
||||
return ctx.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(ctx.src)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should optimise the pdf', function (ctx) {
|
||||
return ctx.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(ctx.src, ctx.dst)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the pdf file is optimised', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, true)
|
||||
ctx.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
ctx.OutputFileOptimiser.optimiseFile(ctx.src, ctx.dst, err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should check if the pdf is optimised', function (ctx) {
|
||||
ctx.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(ctx.src)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not optimise the pdf', function (ctx) {
|
||||
ctx.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(ctx.src, ctx.dst)
|
||||
.should.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkIfPDFISOptimised', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.callback = sinon.stub()
|
||||
ctx.fd = 1234
|
||||
ctx.fs.open = sinon.stub().yields(null, ctx.fd)
|
||||
ctx.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(ctx.fd)
|
||||
.yields(null, 100, Buffer.from('hello /Linearized 1'))
|
||||
ctx.fs.close = sinon.stub().withArgs(ctx.fd).yields(null)
|
||||
ctx.OutputFileOptimiser.checkIfPDFIsOptimised(ctx.src, ctx.callback)
|
||||
})
|
||||
|
||||
describe('for a linearised file', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(ctx.fd)
|
||||
.yields(null, 100, Buffer.from('hello /Linearized 1'))
|
||||
ctx.OutputFileOptimiser.checkIfPDFIsOptimised(ctx.src, ctx.callback)
|
||||
})
|
||||
|
||||
it('should open the file', function (ctx) {
|
||||
ctx.fs.open.calledWith(ctx.src, 'r').should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the header', function (ctx) {
|
||||
ctx.fs.read.calledWith(ctx.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should close the file', function (ctx) {
|
||||
ctx.fs.close.calledWith(ctx.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback with a true result', function (ctx) {
|
||||
ctx.callback.calledWith(null, true).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('for an unlinearised file', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(ctx.fd)
|
||||
.yields(null, 100, Buffer.from('hello not linearized 1'))
|
||||
ctx.OutputFileOptimiser.checkIfPDFIsOptimised(ctx.src, ctx.callback)
|
||||
})
|
||||
|
||||
it('should open the file', function (ctx) {
|
||||
ctx.fs.open.calledWith(ctx.src, 'r').should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the header', function (ctx) {
|
||||
ctx.fs.read.calledWith(ctx.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should close the file', function (ctx) {
|
||||
ctx.fs.close.calledWith(ctx.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback with a false result', function (ctx) {
|
||||
ctx.callback.calledWith(null, false).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,192 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
n/no-deprecated-api,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/OutputFileOptimiser'
|
||||
)
|
||||
const path = require('node:path')
|
||||
const { expect } = require('chai')
|
||||
const { EventEmitter } = require('node:events')
|
||||
|
||||
describe('OutputFileOptimiser', function () {
|
||||
beforeEach(function () {
|
||||
this.OutputFileOptimiser = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
fs: (this.fs = {}),
|
||||
path: (this.Path = {}),
|
||||
child_process: { spawn: (this.spawn = sinon.stub()) },
|
||||
'@overleaf/metrics': {},
|
||||
},
|
||||
globals: { Math }, // used by lodash
|
||||
})
|
||||
this.directory = '/test/dir'
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('optimiseFile', function () {
|
||||
beforeEach(function () {
|
||||
this.src = './output.pdf'
|
||||
return (this.dst = './output.pdf')
|
||||
})
|
||||
|
||||
describe('when the file is not a pdf file', function () {
|
||||
beforeEach(function (done) {
|
||||
this.src = './output.log'
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, false)
|
||||
this.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
|
||||
})
|
||||
|
||||
it('should not check if the file is optimised', function () {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(this.src)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should not optimise the file', function () {
|
||||
return this.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(this.src, this.dst)
|
||||
.should.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the pdf file is not optimised', function () {
|
||||
beforeEach(function (done) {
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, false)
|
||||
this.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
|
||||
})
|
||||
|
||||
it('should check if the pdf is optimised', function () {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(this.src)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should optimise the pdf', function () {
|
||||
return this.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(this.src, this.dst)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the pdf file is optimised', function () {
|
||||
beforeEach(function (done) {
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, true)
|
||||
this.OutputFileOptimiser.optimisePDF = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null)
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done)
|
||||
})
|
||||
|
||||
it('should check if the pdf is optimised', function () {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised
|
||||
.calledWith(this.src)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should not optimise the pdf', function () {
|
||||
return this.OutputFileOptimiser.optimisePDF
|
||||
.calledWith(this.src, this.dst)
|
||||
.should.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('checkIfPDFISOptimised', function () {
|
||||
beforeEach(function () {
|
||||
this.callback = sinon.stub()
|
||||
this.fd = 1234
|
||||
this.fs.open = sinon.stub().yields(null, this.fd)
|
||||
this.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(this.fd)
|
||||
.yields(null, 100, Buffer.from('hello /Linearized 1'))
|
||||
this.fs.close = sinon.stub().withArgs(this.fd).yields(null)
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
|
||||
this.src,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
describe('for a linearised file', function () {
|
||||
beforeEach(function () {
|
||||
this.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(this.fd)
|
||||
.yields(null, 100, Buffer.from('hello /Linearized 1'))
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
|
||||
this.src,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the file', function () {
|
||||
return this.fs.open.calledWith(this.src, 'r').should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the header', function () {
|
||||
return this.fs.read.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should close the file', function () {
|
||||
return this.fs.close.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with a true result', function () {
|
||||
return this.callback.calledWith(null, true).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('for an unlinearised file', function () {
|
||||
beforeEach(function () {
|
||||
this.fs.read = sinon
|
||||
.stub()
|
||||
.withArgs(this.fd)
|
||||
.yields(null, 100, Buffer.from('hello not linearized 1'))
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(
|
||||
this.src,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the file', function () {
|
||||
return this.fs.open.calledWith(this.src, 'r').should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the header', function () {
|
||||
return this.fs.read.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
it('should close the file', function () {
|
||||
return this.fs.close.calledWith(this.fd).should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with a false result', function () {
|
||||
return this.callback.calledWith(null, false).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,182 @@
|
||||
import { vi, describe, beforeEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import path from 'node:path'
|
||||
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/ProjectPersistenceManager'
|
||||
)
|
||||
|
||||
describe('ProjectPersistenceManager', () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.fsPromises = {
|
||||
statfs: sinon.stub(),
|
||||
}
|
||||
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: (ctx.Metrics = { gauge: sinon.stub() }),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/UrlCache', () => ({
|
||||
default: (ctx.UrlCache = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/CompileManager', () => ({
|
||||
default: (ctx.CompileManager = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('fs', () => ({
|
||||
default: { promises: ctx.fsPromises },
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: (ctx.settings = {
|
||||
project_cache_length_ms: 1000,
|
||||
path: {
|
||||
compilesDir: '/compiles',
|
||||
outputDir: '/output',
|
||||
clsiCacheDir: '/cache',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
ctx.ProjectPersistenceManager = (await import(modulePath)).default
|
||||
ctx.callback = sinon.stub()
|
||||
ctx.project_id = 'project-id-123'
|
||||
return (ctx.user_id = '1234')
|
||||
})
|
||||
|
||||
describe('refreshExpiryTimeout', () => {
|
||||
it('should leave expiry alone if plenty of disk', async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 40,
|
||||
})
|
||||
|
||||
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
ctx.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
40
|
||||
)
|
||||
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(
|
||||
ctx.settings.project_cache_length_ms
|
||||
)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should drop EXPIRY_TIMEOUT 10% if low disk usage', async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 5,
|
||||
})
|
||||
|
||||
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
ctx.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
5
|
||||
)
|
||||
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 5,
|
||||
})
|
||||
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500
|
||||
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
ctx.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
5
|
||||
)
|
||||
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.fsPromises.statfs.rejects(new Error())
|
||||
ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearExpiredProjects', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.project_ids = ['project-id-1', 'project-id-2']
|
||||
ctx.ProjectPersistenceManager._findExpiredProjectIds = sinon
|
||||
.stub()
|
||||
.callsArgWith(0, null, ctx.project_ids)
|
||||
ctx.ProjectPersistenceManager.clearProjectFromCache = sinon
|
||||
.stub()
|
||||
.callsArg(2)
|
||||
ctx.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1)
|
||||
return ctx.ProjectPersistenceManager.clearExpiredProjects(ctx.callback)
|
||||
})
|
||||
|
||||
it('should clear each expired project', ctx => {
|
||||
return Array.from(ctx.project_ids).map(projectId =>
|
||||
ctx.ProjectPersistenceManager.clearProjectFromCache
|
||||
.calledWith(projectId)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', ctx => {
|
||||
return ctx.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('clearProject', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.ProjectPersistenceManager._clearProjectFromDatabase = sinon
|
||||
.stub()
|
||||
.callsArg(1)
|
||||
ctx.UrlCache.clearProject = sinon.stub().callsArg(2)
|
||||
ctx.CompileManager.clearProject = sinon.stub().callsArg(2)
|
||||
return ctx.ProjectPersistenceManager.clearProject(
|
||||
ctx.project_id,
|
||||
ctx.user_id,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear the project from the database', ctx => {
|
||||
return ctx.ProjectPersistenceManager._clearProjectFromDatabase
|
||||
.calledWith(ctx.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should clear all the cached Urls for the project', ctx => {
|
||||
return ctx.UrlCache.clearProject
|
||||
.calledWith(ctx.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should clear the project compile folder', ctx => {
|
||||
return ctx.CompileManager.clearProject
|
||||
.calledWith(ctx.project_id, ctx.user_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', ctx => {
|
||||
return ctx.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,174 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const assert = require('chai').assert
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ProjectPersistenceManager'
|
||||
)
|
||||
const tk = require('timekeeper')
|
||||
|
||||
describe('ProjectPersistenceManager', function () {
|
||||
beforeEach(function () {
|
||||
this.fsPromises = {
|
||||
statfs: sinon.stub(),
|
||||
}
|
||||
|
||||
this.ProjectPersistenceManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/metrics': (this.Metrics = { gauge: sinon.stub() }),
|
||||
'./UrlCache': (this.UrlCache = {}),
|
||||
'./CompileManager': (this.CompileManager = {}),
|
||||
fs: { promises: this.fsPromises },
|
||||
'@overleaf/settings': (this.settings = {
|
||||
project_cache_length_ms: 1000,
|
||||
path: {
|
||||
compilesDir: '/compiles',
|
||||
outputDir: '/output',
|
||||
clsiCacheDir: '/cache',
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
this.callback = sinon.stub()
|
||||
this.project_id = 'project-id-123'
|
||||
return (this.user_id = '1234')
|
||||
})
|
||||
|
||||
describe('refreshExpiryTimeout', function () {
|
||||
it('should leave expiry alone if plenty of disk', function (done) {
|
||||
this.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 40,
|
||||
})
|
||||
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
40
|
||||
)
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(
|
||||
this.settings.project_cache_length_ms
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should drop EXPIRY_TIMEOUT 10% if low disk usage', function (done) {
|
||||
this.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 5,
|
||||
})
|
||||
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
5
|
||||
)
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', function (done) {
|
||||
this.fsPromises.statfs.resolves({
|
||||
blocks: 100,
|
||||
bsize: 1,
|
||||
bavail: 5,
|
||||
})
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.Metrics.gauge.should.have.been.calledWith(
|
||||
'disk_available_percent',
|
||||
5
|
||||
)
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', function (done) {
|
||||
this.fsPromises.statfs.rejects(new Error())
|
||||
this.ProjectPersistenceManager.refreshExpiryTimeout(() => {
|
||||
this.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearExpiredProjects', function () {
|
||||
beforeEach(function () {
|
||||
this.project_ids = ['project-id-1', 'project-id-2']
|
||||
this.ProjectPersistenceManager._findExpiredProjectIds = sinon
|
||||
.stub()
|
||||
.callsArgWith(0, null, this.project_ids)
|
||||
this.ProjectPersistenceManager.clearProjectFromCache = sinon
|
||||
.stub()
|
||||
.callsArg(2)
|
||||
this.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1)
|
||||
return this.ProjectPersistenceManager.clearExpiredProjects(this.callback)
|
||||
})
|
||||
|
||||
it('should clear each expired project', function () {
|
||||
return Array.from(this.project_ids).map(projectId =>
|
||||
this.ProjectPersistenceManager.clearProjectFromCache
|
||||
.calledWith(projectId)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('clearProject', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectPersistenceManager._clearProjectFromDatabase = sinon
|
||||
.stub()
|
||||
.callsArg(1)
|
||||
this.UrlCache.clearProject = sinon.stub().callsArg(2)
|
||||
this.CompileManager.clearProject = sinon.stub().callsArg(2)
|
||||
return this.ProjectPersistenceManager.clearProject(
|
||||
this.project_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear the project from the database', function () {
|
||||
return this.ProjectPersistenceManager._clearProjectFromDatabase
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should clear all the cached Urls for the project', function () {
|
||||
return this.UrlCache.clearProject
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should clear the project compile folder', function () {
|
||||
return this.CompileManager.clearProject
|
||||
.calledWith(this.project_id, this.user_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,502 @@
|
||||
import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import tk from 'timekeeper'
|
||||
import path from 'node:path'
|
||||
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/RequestParser'
|
||||
)
|
||||
|
||||
describe('RequestParser', () => {
|
||||
beforeEach(async ctx => {
|
||||
tk.freeze()
|
||||
ctx.callback = sinon.stub()
|
||||
ctx.validResource = {
|
||||
path: 'main.tex',
|
||||
date: '12:00 01/02/03',
|
||||
content: 'Hello world',
|
||||
}
|
||||
ctx.validRequest = {
|
||||
compile: {
|
||||
token: 'token-123',
|
||||
options: {
|
||||
imageName: 'basicImageName/here:2017-1',
|
||||
compiler: 'pdflatex',
|
||||
timeout: 42,
|
||||
},
|
||||
resources: [],
|
||||
},
|
||||
}
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: (ctx.settings = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/OutputCacheManager', () => ({
|
||||
default: { BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/ },
|
||||
}))
|
||||
|
||||
ctx.RequestParser = (await import(modulePath)).default
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
tk.reset()
|
||||
})
|
||||
|
||||
describe('without a top level object', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.RequestParser.parse([], ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
expect(ctx.callback).to.have.been.called
|
||||
expect(ctx.callback.args[0][0].message).to.equal(
|
||||
'top level object should have a compile attribute'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a compile attribute', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.RequestParser.parse({}, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
expect(ctx.callback).to.have.been.called
|
||||
expect(ctx.callback.args[0][0].message).to.equal(
|
||||
'top level object should have a compile attribute'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a valid compiler', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validRequest.compile.options.compiler = 'not-a-compiler'
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'compiler attribute should be one of: pdflatex, latex, xelatex, lualatex',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a compiler specified', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
delete ctx.validRequest.compile.options.compiler
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the compiler to pdflatex by default', ctx => {
|
||||
ctx.data.compiler.should.equal('pdflatex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with imageName set', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the imageName', ctx => {
|
||||
ctx.data.imageName.should.equal('basicImageName/here:2017-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when image restrictions are present', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.settings.clsi = { docker: {} }
|
||||
ctx.settings.clsi.docker.allowedImages = [
|
||||
'repo/name:tag1',
|
||||
'repo/name:tag2',
|
||||
]
|
||||
})
|
||||
|
||||
describe('with imageName set to something invalid', () => {
|
||||
beforeEach(ctx => {
|
||||
const request = ctx.validRequest
|
||||
request.compile.options.imageName = 'something/different:latest'
|
||||
ctx.RequestParser.parse(request, (error, data) => {
|
||||
ctx.error = error
|
||||
ctx.data = data
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error for imageName', ctx => {
|
||||
expect(String(ctx.error)).to.include(
|
||||
'imageName attribute should be one of'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with imageName set to something valid', () => {
|
||||
beforeEach(ctx => {
|
||||
const request = ctx.validRequest
|
||||
request.compile.options.imageName = 'repo/name:tag1'
|
||||
ctx.RequestParser.parse(request, (error, data) => {
|
||||
ctx.error = error
|
||||
ctx.data = data
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the imageName', ctx => {
|
||||
ctx.data.imageName.should.equal('repo/name:tag1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with flags set', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.validRequest.compile.options.flags = ['-file-line-error']
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the flags attribute', ctx => {
|
||||
expect(ctx.data.flags).to.deep.equal(['-file-line-error'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with flags not specified', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('it should have an empty flags list', ctx => {
|
||||
expect(ctx.data.flags).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a timeout specified', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
delete ctx.validRequest.compile.options.timeout
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout to MAX_TIMEOUT', ctx => {
|
||||
ctx.data.timeout.should.equal(ctx.RequestParser.MAX_TIMEOUT * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a timeout larger than the maximum', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.validRequest.compile.options.timeout =
|
||||
ctx.RequestParser.MAX_TIMEOUT + 1
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout to MAX_TIMEOUT', ctx => {
|
||||
ctx.data.timeout.should.equal(ctx.RequestParser.MAX_TIMEOUT * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a timeout', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout (in milliseconds)', ctx => {
|
||||
ctx.data.timeout.should.equal(
|
||||
ctx.validRequest.compile.options.timeout * 1000
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource without a path', () => {
|
||||
beforeEach(ctx => {
|
||||
delete ctx.validResource.path
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({
|
||||
message: 'all resources should have a path attribute',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a path', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validResource.path = ctx.path = 'test.tex'
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the path in the parsed response', ctx => {
|
||||
ctx.data.resources[0].path.should.equal(ctx.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a malformed modified date', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validResource.modified = 'not-a-date'
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'resource modified date could not be understood: ' +
|
||||
ctx.validResource.modified,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a valid buildId', () => {
|
||||
beforeEach(async ctx => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.validRequest.compile.options.buildId =
|
||||
'195a4869176-a4ad60bee7bf35e4'
|
||||
ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
|
||||
if (error) return reject(error)
|
||||
ctx.data = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.data.buildId.should.equal('195a4869176-a4ad60bee7bf35e4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a bad buildId', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validRequest.compile.options.buildId = 'foo/bar'
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'buildId attribute does not match regex /^[0-9a-f]+-[0-9a-f]+$/',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a valid date', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.date = '12:00 01/02/03'
|
||||
ctx.validResource.modified = ctx.date
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the date as a Javascript Date object', ctx => {
|
||||
;(ctx.data.resources[0].modified instanceof Date).should.equal(true)
|
||||
ctx.data.resources[0].modified
|
||||
.getTime()
|
||||
.should.equal(Date.parse(ctx.date))
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource without either a content or URL attribute', () => {
|
||||
beforeEach(ctx => {
|
||||
delete ctx.validResource.url
|
||||
delete ctx.validResource.content
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'all resources should have either a url or content attribute',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource where the content is not a string', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validResource.content = []
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({ message: 'content attribute should be a string' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource where the url is not a string', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validResource.url = []
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({ message: 'url attribute should be a string' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a url', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validResource.url = ctx.url = 'www.example.com'
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the url in the parsed response', ctx => {
|
||||
ctx.data.resources[0].url.should.equal(ctx.url)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a content attribute', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validResource.content = ctx.content = 'Hello world'
|
||||
ctx.validRequest.compile.resources.push(ctx.validResource)
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the content in the parsed response', ctx => {
|
||||
ctx.data.resources[0].content.should.equal(ctx.content)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a root resource path', () => {
|
||||
beforeEach(ctx => {
|
||||
delete ctx.validRequest.compile.rootResourcePath
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it("should set the root resource path to 'main.tex' by default", ctx => {
|
||||
ctx.data.rootResourcePath.should.equal('main.tex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validRequest.compile.rootResourcePath = ctx.path = 'test.tex'
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the root resource path in the parsed response', ctx => {
|
||||
ctx.data.rootResourcePath.should.equal(ctx.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that is not a string', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validRequest.compile.rootResourcePath = []
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({
|
||||
message: 'rootResourcePath attribute should be a string',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that has a relative path', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validRequest.compile.rootResourcePath = 'foo/../../bar.tex'
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({ message: 'relative path in root resource' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that has unescaped + relative path', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validRequest.compile.rootResourcePath = 'foo/../bar.tex'
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({ message: 'relative path in root resource' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an unknown syncType', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.validRequest.compile.options.syncType = 'unexpected'
|
||||
ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
|
||||
ctx.data = ctx.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWithMatch({
|
||||
message: 'syncType attribute should be one of: full, incremental',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,480 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/RequestParser'
|
||||
)
|
||||
const tk = require('timekeeper')
|
||||
|
||||
describe('RequestParser', function () {
|
||||
beforeEach(function () {
|
||||
tk.freeze()
|
||||
this.callback = sinon.stub()
|
||||
this.validResource = {
|
||||
path: 'main.tex',
|
||||
date: '12:00 01/02/03',
|
||||
content: 'Hello world',
|
||||
}
|
||||
this.validRequest = {
|
||||
compile: {
|
||||
token: 'token-123',
|
||||
options: {
|
||||
imageName: 'basicImageName/here:2017-1',
|
||||
compiler: 'pdflatex',
|
||||
timeout: 42,
|
||||
},
|
||||
resources: [],
|
||||
},
|
||||
}
|
||||
this.RequestParser = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': (this.settings = {}),
|
||||
'./OutputCacheManager': { BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/ },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
tk.reset()
|
||||
})
|
||||
|
||||
describe('without a top level object', function () {
|
||||
beforeEach(function () {
|
||||
this.RequestParser.parse([], this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
expect(this.callback).to.have.been.called
|
||||
expect(this.callback.args[0][0].message).to.equal(
|
||||
'top level object should have a compile attribute'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a compile attribute', function () {
|
||||
beforeEach(function () {
|
||||
this.RequestParser.parse({}, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
expect(this.callback).to.have.been.called
|
||||
expect(this.callback.args[0][0].message).to.equal(
|
||||
'top level object should have a compile attribute'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a valid compiler', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.options.compiler = 'not-a-compiler'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'compiler attribute should be one of: pdflatex, latex, xelatex, lualatex',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a compiler specified', function () {
|
||||
beforeEach(function (done) {
|
||||
delete this.validRequest.compile.options.compiler
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the compiler to pdflatex by default', function () {
|
||||
this.data.compiler.should.equal('pdflatex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with imageName set', function () {
|
||||
beforeEach(function (done) {
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the imageName', function () {
|
||||
this.data.imageName.should.equal('basicImageName/here:2017-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when image restrictions are present', function () {
|
||||
beforeEach(function () {
|
||||
this.settings.clsi = { docker: {} }
|
||||
this.settings.clsi.docker.allowedImages = [
|
||||
'repo/name:tag1',
|
||||
'repo/name:tag2',
|
||||
]
|
||||
})
|
||||
|
||||
describe('with imageName set to something invalid', function () {
|
||||
beforeEach(function () {
|
||||
const request = this.validRequest
|
||||
request.compile.options.imageName = 'something/different:latest'
|
||||
this.RequestParser.parse(request, (error, data) => {
|
||||
this.error = error
|
||||
this.data = data
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error for imageName', function () {
|
||||
expect(String(this.error)).to.include(
|
||||
'imageName attribute should be one of'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with imageName set to something valid', function () {
|
||||
beforeEach(function () {
|
||||
const request = this.validRequest
|
||||
request.compile.options.imageName = 'repo/name:tag1'
|
||||
this.RequestParser.parse(request, (error, data) => {
|
||||
this.error = error
|
||||
this.data = data
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the imageName', function () {
|
||||
this.data.imageName.should.equal('repo/name:tag1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with flags set', function () {
|
||||
beforeEach(function (done) {
|
||||
this.validRequest.compile.options.flags = ['-file-line-error']
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the flags attribute', function () {
|
||||
expect(this.data.flags).to.deep.equal(['-file-line-error'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with flags not specified', function () {
|
||||
beforeEach(function (done) {
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('it should have an empty flags list', function () {
|
||||
expect(this.data.flags).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a timeout specified', function () {
|
||||
beforeEach(function (done) {
|
||||
delete this.validRequest.compile.options.timeout
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout to MAX_TIMEOUT', function () {
|
||||
this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a timeout larger than the maximum', function () {
|
||||
beforeEach(function (done) {
|
||||
this.validRequest.compile.options.timeout =
|
||||
this.RequestParser.MAX_TIMEOUT + 1
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout to MAX_TIMEOUT', function () {
|
||||
this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a timeout', function () {
|
||||
beforeEach(function (done) {
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the timeout (in milliseconds)', function () {
|
||||
this.data.timeout.should.equal(
|
||||
this.validRequest.compile.options.timeout * 1000
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource without a path', function () {
|
||||
beforeEach(function () {
|
||||
delete this.validResource.path
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message: 'all resources should have a path attribute',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a path', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.path = this.path = 'test.tex'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the path in the parsed response', function () {
|
||||
this.data.resources[0].path.should.equal(this.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a malformed modified date', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.modified = 'not-a-date'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'resource modified date could not be understood: ' +
|
||||
this.validResource.modified,
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a valid buildId', function () {
|
||||
beforeEach(function (done) {
|
||||
this.validRequest.compile.options.buildId = '195a4869176-a4ad60bee7bf35e4'
|
||||
this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
if (error) return done(error)
|
||||
this.data = data
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.data.buildId.should.equal('195a4869176-a4ad60bee7bf35e4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a bad buildId', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.options.buildId = 'foo/bar'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'buildId attribute does not match regex /^[0-9a-f]+-[0-9a-f]+$/',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a valid date', function () {
|
||||
beforeEach(function () {
|
||||
this.date = '12:00 01/02/03'
|
||||
this.validResource.modified = this.date
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the date as a Javascript Date object', function () {
|
||||
;(this.data.resources[0].modified instanceof Date).should.equal(true)
|
||||
this.data.resources[0].modified
|
||||
.getTime()
|
||||
.should.equal(Date.parse(this.date))
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource without either a content or URL attribute', function () {
|
||||
beforeEach(function () {
|
||||
delete this.validResource.url
|
||||
delete this.validResource.content
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message:
|
||||
'all resources should have either a url or content attribute',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource where the content is not a string', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.content = []
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'content attribute should be a string' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource where the url is not a string', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.url = []
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'url attribute should be a string' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a url', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.url = this.url = 'www.example.com'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the url in the parsed response', function () {
|
||||
this.data.resources[0].url.should.equal(this.url)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a resource with a content attribute', function () {
|
||||
beforeEach(function () {
|
||||
this.validResource.content = this.content = 'Hello world'
|
||||
this.validRequest.compile.resources.push(this.validResource)
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the content in the parsed response', function () {
|
||||
this.data.resources[0].content.should.equal(this.content)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without a root resource path', function () {
|
||||
beforeEach(function () {
|
||||
delete this.validRequest.compile.rootResourcePath
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it("should set the root resource path to 'main.tex' by default", function () {
|
||||
this.data.rootResourcePath.should.equal('main.tex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = this.path = 'test.tex'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return the root resource path in the parsed response', function () {
|
||||
this.data.rootResourcePath.should.equal(this.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that is not a string', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = []
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message: 'rootResourcePath attribute should be a string',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that has a relative path', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = 'foo/../../bar.tex'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'relative path in root resource' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a root resource path that has unescaped + relative path', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.rootResourcePath = 'foo/../bar.tex'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({ message: 'relative path in root resource' })
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an unknown syncType', function () {
|
||||
beforeEach(function () {
|
||||
this.validRequest.compile.options.syncType = 'unexpected'
|
||||
this.RequestParser.parse(this.validRequest, this.callback)
|
||||
this.data = this.callback.args[0][1]
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback
|
||||
.calledWithMatch({
|
||||
message: 'syncType attribute should be one of: full, incremental',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,229 @@
|
||||
import { vi, expect, describe, beforeEach, it } from 'vitest'
|
||||
|
||||
import sinon from 'sinon'
|
||||
import Path from 'node:path'
|
||||
import * as Errors from '../../../app/js/Errors.js'
|
||||
|
||||
const modulePath = Path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/ResourceStateManager'
|
||||
)
|
||||
|
||||
describe('ResourceStateManager', () => {
|
||||
beforeEach(async ctx => {
|
||||
vi.doMock('fs', () => ({
|
||||
default: (ctx.fs = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/SafeReader', () => ({
|
||||
default: (ctx.SafeReader = {}),
|
||||
}))
|
||||
|
||||
ctx.ResourceStateManager = (await import(modulePath)).default
|
||||
ctx.basePath = '/path/to/write/files/to'
|
||||
ctx.resources = [
|
||||
{ path: 'resource-1-mock' },
|
||||
{ path: 'resource-2-mock' },
|
||||
{ path: 'resource-3-mock' },
|
||||
]
|
||||
ctx.state = '1234567890'
|
||||
ctx.resourceFileName = `${ctx.basePath}/.project-sync-state`
|
||||
ctx.resourceFileContents = `${ctx.resources[0].path}\n${ctx.resources[1].path}\n${ctx.resources[2].path}\nstateHash:${ctx.state}`
|
||||
ctx.callback = sinon.stub()
|
||||
})
|
||||
|
||||
describe('saveProjectState', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.fs.writeFile = sinon.stub().callsArg(2)
|
||||
})
|
||||
|
||||
describe('when the state is specified', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.ResourceStateManager.saveProjectState(
|
||||
ctx.state,
|
||||
ctx.resources,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should write the resource list to disk', ctx => {
|
||||
ctx.fs.writeFile
|
||||
.calledWith(ctx.resourceFileName, ctx.resourceFileContents)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback', ctx => {
|
||||
ctx.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the state is undefined', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.state = undefined
|
||||
ctx.fs.unlink = sinon.stub().callsArg(1)
|
||||
ctx.ResourceStateManager.saveProjectState(
|
||||
ctx.state,
|
||||
ctx.resources,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should unlink the resource file', ctx => {
|
||||
ctx.fs.unlink.calledWith(ctx.resourceFileName).should.equal(true)
|
||||
})
|
||||
|
||||
it('should not write the resource list to disk', ctx => {
|
||||
ctx.fs.writeFile.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should call the callback', ctx => {
|
||||
ctx.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkProjectStateMatches', () => {
|
||||
describe('when the state matches', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, ctx.resourceFileContents)
|
||||
ctx.ResourceStateManager.checkProjectStateMatches(
|
||||
ctx.state,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should read the resource file', ctx => {
|
||||
ctx.SafeReader.readFile
|
||||
.calledWith(ctx.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback with the results', ctx => {
|
||||
ctx.callback.calledWithMatch(null, ctx.resources).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the state file is not present', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.SafeReader.readFile = sinon.stub().callsArg(3)
|
||||
ctx.ResourceStateManager.checkProjectStateMatches(
|
||||
ctx.state,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should read the resource file', ctx => {
|
||||
ctx.SafeReader.readFile
|
||||
.calledWith(ctx.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = ctx.callback.args[0][0].message
|
||||
expect(message).to.include('invalid state for incremental update')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the state does not match', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, ctx.resourceFileContents)
|
||||
ctx.ResourceStateManager.checkProjectStateMatches(
|
||||
'not-the-original-state',
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = ctx.callback.args[0][0].message
|
||||
expect(message).to.include('invalid state for incremental update')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkResourceFiles', () => {
|
||||
describe('when all the files are present', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.allFiles = [
|
||||
ctx.resources[0].path,
|
||||
ctx.resources[1].path,
|
||||
ctx.resources[2].path,
|
||||
]
|
||||
ctx.ResourceStateManager.checkResourceFiles(
|
||||
ctx.resources,
|
||||
ctx.allFiles,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback', ctx => {
|
||||
ctx.callback.calledWithExactly().should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a missing file', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.allFiles = [ctx.resources[0].path, ctx.resources[1].path]
|
||||
ctx.fs.stat = sinon.stub().callsArgWith(1, new Error())
|
||||
ctx.ResourceStateManager.checkResourceFiles(
|
||||
ctx.resources,
|
||||
ctx.allFiles,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', ctx => {
|
||||
ctx.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = ctx.callback.args[0][0].message
|
||||
expect(message).to.include(
|
||||
'resource files missing in incremental update'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a resource contains a relative path', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.resources[0].path = '../foo/bar.tex'
|
||||
ctx.allFiles = [
|
||||
ctx.resources[0].path,
|
||||
ctx.resources[1].path,
|
||||
ctx.resources[2].path,
|
||||
]
|
||||
ctx.ResourceStateManager.checkResourceFiles(
|
||||
ctx.resources,
|
||||
ctx.allFiles,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', ctx => {
|
||||
ctx.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = ctx.callback.args[0][0].message
|
||||
expect(message).to.include('relative path in resource file list')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,241 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ResourceStateManager'
|
||||
)
|
||||
const Path = require('node:path')
|
||||
const Errors = require('../../../app/js/Errors')
|
||||
|
||||
describe('ResourceStateManager', function () {
|
||||
beforeEach(function () {
|
||||
this.ResourceStateManager = SandboxedModule.require(modulePath, {
|
||||
singleOnly: true,
|
||||
requires: {
|
||||
fs: (this.fs = {}),
|
||||
'./SafeReader': (this.SafeReader = {}),
|
||||
},
|
||||
})
|
||||
this.basePath = '/path/to/write/files/to'
|
||||
this.resources = [
|
||||
{ path: 'resource-1-mock' },
|
||||
{ path: 'resource-2-mock' },
|
||||
{ path: 'resource-3-mock' },
|
||||
]
|
||||
this.state = '1234567890'
|
||||
this.resourceFileName = `${this.basePath}/.project-sync-state`
|
||||
this.resourceFileContents = `${this.resources[0].path}\n${this.resources[1].path}\n${this.resources[2].path}\nstateHash:${this.state}`
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('saveProjectState', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.writeFile = sinon.stub().callsArg(2))
|
||||
})
|
||||
|
||||
describe('when the state is specified', function () {
|
||||
beforeEach(function () {
|
||||
return this.ResourceStateManager.saveProjectState(
|
||||
this.state,
|
||||
this.resources,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should write the resource list to disk', function () {
|
||||
return this.fs.writeFile
|
||||
.calledWith(this.resourceFileName, this.resourceFileContents)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the state is undefined', function () {
|
||||
beforeEach(function () {
|
||||
this.state = undefined
|
||||
this.fs.unlink = sinon.stub().callsArg(1)
|
||||
return this.ResourceStateManager.saveProjectState(
|
||||
this.state,
|
||||
this.resources,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should unlink the resource file', function () {
|
||||
return this.fs.unlink
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not write the resource list to disk', function () {
|
||||
return this.fs.writeFile.called.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkProjectStateMatches', function () {
|
||||
describe('when the state matches', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, this.resourceFileContents)
|
||||
return this.ResourceStateManager.checkProjectStateMatches(
|
||||
this.state,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should read the resource file', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with the results', function () {
|
||||
return this.callback
|
||||
.calledWithMatch(null, this.resources)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the state file is not present', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon.stub().callsArg(3)
|
||||
return this.ResourceStateManager.checkProjectStateMatches(
|
||||
this.state,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should read the resource file', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('invalid state for incremental update')
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when the state does not match', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, this.resourceFileContents)
|
||||
return this.ResourceStateManager.checkProjectStateMatches(
|
||||
'not-the-original-state',
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('invalid state for incremental update')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('checkResourceFiles', function () {
|
||||
describe('when all the files are present', function () {
|
||||
beforeEach(function () {
|
||||
this.allFiles = [
|
||||
this.resources[0].path,
|
||||
this.resources[1].path,
|
||||
this.resources[2].path,
|
||||
]
|
||||
return this.ResourceStateManager.checkResourceFiles(
|
||||
this.resources,
|
||||
this.allFiles,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.calledWithExactly().should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a missing file', function () {
|
||||
beforeEach(function () {
|
||||
this.allFiles = [this.resources[0].path, this.resources[1].path]
|
||||
this.fs.stat = sinon.stub().callsArgWith(1, new Error())
|
||||
return this.ResourceStateManager.checkResourceFiles(
|
||||
this.resources,
|
||||
this.allFiles,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match(Errors.FilesOutOfSyncError))
|
||||
.should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include(
|
||||
'resource files missing in incremental update'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('when a resource contains a relative path', function () {
|
||||
beforeEach(function () {
|
||||
this.resources[0].path = '../foo/bar.tex'
|
||||
this.allFiles = [
|
||||
this.resources[0].path,
|
||||
this.resources[1].path,
|
||||
this.resources[2].path,
|
||||
]
|
||||
return this.ResourceStateManager.checkResourceFiles(
|
||||
this.resources,
|
||||
this.allFiles,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the callback with an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('relative path in resource file list')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,536 @@
|
||||
import { vi, expect, describe, beforeEach, it } from 'vitest'
|
||||
|
||||
import sinon from 'sinon'
|
||||
import path from 'node:path'
|
||||
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/ResourceWriter'
|
||||
)
|
||||
|
||||
describe('ResourceWriter', () => {
|
||||
beforeEach(async ctx => {
|
||||
let Timer
|
||||
|
||||
vi.doMock('fs', () => ({
|
||||
default: (ctx.fs = {
|
||||
mkdir: sinon.stub().callsArg(1),
|
||||
unlink: sinon.stub().callsArg(1),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/ResourceStateManager', () => ({
|
||||
default: (ctx.ResourceStateManager = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/UrlCache', () => ({
|
||||
default: (ctx.UrlCache = {
|
||||
createProjectDir: sinon.stub().yields(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../app/js/OutputFileFinder', () => ({
|
||||
default: (ctx.OutputFileFinder = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
// Mocks allow us to import Metrics.js twice without getting errors.
|
||||
prom: {
|
||||
Gauge: sinon.stub(),
|
||||
Histogram: sinon.stub(),
|
||||
Counter: sinon.stub(),
|
||||
},
|
||||
default: (ctx.Metrics = {
|
||||
inc: sinon.stub(),
|
||||
Timer: (Timer = (function () {
|
||||
Timer = class Timer {
|
||||
static initClass() {
|
||||
this.prototype.done = sinon.stub()
|
||||
}
|
||||
}
|
||||
Timer.initClass()
|
||||
return Timer
|
||||
})()),
|
||||
}),
|
||||
}))
|
||||
|
||||
ctx.ResourceWriter = (await import(modulePath)).default
|
||||
ctx.project_id = 'project-id-123'
|
||||
ctx.basePath = '/path/to/write/files/to'
|
||||
return (ctx.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on a full request', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.resources = ['resource-1-mock', 'resource-2-mock', 'resource-3-mock']
|
||||
ctx.request = {
|
||||
project_id: ctx.project_id,
|
||||
syncState: (ctx.syncState = '0123456789abcdef'),
|
||||
resources: ctx.resources,
|
||||
}
|
||||
ctx.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
|
||||
ctx.ResourceWriter._removeExtraneousFiles = sinon.stub().yields(null)
|
||||
ctx.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
|
||||
return ctx.ResourceWriter.syncResourcesToDisk(
|
||||
ctx.request,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should remove old files', ctx => {
|
||||
return ctx.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(ctx.request, ctx.resources, ctx.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write each resource to disk', ctx => {
|
||||
return Array.from(ctx.resources).map(resource =>
|
||||
ctx.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(ctx.project_id, resource, ctx.basePath)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
it('should store the sync state and resource list', ctx => {
|
||||
return ctx.ResourceStateManager.saveProjectState
|
||||
.calledWith(ctx.syncState, ctx.resources, ctx.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', ctx => {
|
||||
return ctx.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on an incremental update', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.resources = ['resource-1-mock']
|
||||
ctx.request = {
|
||||
project_id: ctx.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (ctx.syncState = '1234567890abcdef'),
|
||||
resources: ctx.resources,
|
||||
}
|
||||
ctx.fullResources = ctx.resources.concat(['file-1'])
|
||||
ctx.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
|
||||
ctx.ResourceWriter._removeExtraneousFiles = sinon
|
||||
.stub()
|
||||
.yields(null, (ctx.outputFiles = []), (ctx.allFiles = []))
|
||||
ctx.ResourceStateManager.checkProjectStateMatches = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, ctx.fullResources)
|
||||
ctx.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
|
||||
ctx.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3)
|
||||
return ctx.ResourceWriter.syncResourcesToDisk(
|
||||
ctx.request,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should check the sync state matches', ctx => {
|
||||
return ctx.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(ctx.syncState, ctx.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should remove old files', ctx => {
|
||||
return ctx.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(ctx.request, ctx.fullResources, ctx.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should check each resource exists', ctx => {
|
||||
return ctx.ResourceStateManager.checkResourceFiles
|
||||
.calledWith(ctx.fullResources, ctx.allFiles, ctx.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write each resource to disk', ctx => {
|
||||
return Array.from(ctx.resources).map(resource =>
|
||||
ctx.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(ctx.project_id, resource, ctx.basePath)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', ctx => {
|
||||
return ctx.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on an incremental update when the state does not match', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.resources = ['resource-1-mock']
|
||||
ctx.request = {
|
||||
project_id: ctx.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (ctx.syncState = '1234567890abcdef'),
|
||||
resources: ctx.resources,
|
||||
}
|
||||
ctx.ResourceStateManager.checkProjectStateMatches = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, (ctx.error = new Error()))
|
||||
return ctx.ResourceWriter.syncResourcesToDisk(
|
||||
ctx.request,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should check whether the sync state matches', ctx => {
|
||||
return ctx.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(ctx.syncState, ctx.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with an error', ctx => {
|
||||
return ctx.callback.calledWith(ctx.error).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_removeExtraneousFiles', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'extra/file.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
{
|
||||
path: 'extra.aux',
|
||||
type: 'aux',
|
||||
},
|
||||
{
|
||||
path: 'cache/_chunk1',
|
||||
},
|
||||
{
|
||||
path: 'figures/image-eps-converted-to.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.md5',
|
||||
type: 'md5',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.dpth',
|
||||
type: 'dpth',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/default-pyg-prefix.pygstyle',
|
||||
type: 'pygstyle',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/default.pygstyle',
|
||||
type: 'pygstyle',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex',
|
||||
type: 'pygtex',
|
||||
},
|
||||
{
|
||||
path: '_markdown_main/30893013dec5d869a415610079774c2f.md.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
{
|
||||
path: 'output.stdout',
|
||||
},
|
||||
{
|
||||
path: 'output.stderr',
|
||||
},
|
||||
]
|
||||
ctx.resources = 'mock-resources'
|
||||
ctx.request = {
|
||||
project_id: ctx.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (ctx.syncState = '1234567890abcdef'),
|
||||
resources: ctx.resources,
|
||||
metricsOpts: { path: 'foo' },
|
||||
}
|
||||
ctx.OutputFileFinder.findOutputFiles = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, ctx.output_files)
|
||||
ctx.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1)
|
||||
return ctx.ResourceWriter._removeExtraneousFiles(
|
||||
ctx.request,
|
||||
ctx.resources,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should find the existing output files', ctx => {
|
||||
return ctx.OutputFileFinder.findOutputFiles
|
||||
.calledWith(ctx.resources, ctx.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the output files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'output.pdf'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the stdout log file', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'output.stdout'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the stderr log file', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'output.stderr'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the extra files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'extra/file.tex'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not delete the extra aux files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'extra.aux'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the knitr cache file', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'cache/_chunk1'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the epstopdf converted files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(ctx.basePath, 'figures/image-eps-converted-to.pdf')
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz md5 files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'foo/main-figure0.md5'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz dpth files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'foo/main-figure0.dpth'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz pdf files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, 'foo/main-figure0.pdf'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted pygstyle files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(ctx.basePath, '_minted-main/default-pyg-prefix.pygstyle')
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted default pygstyle files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(ctx.basePath, '_minted-main/default.pygstyle'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted default pygtex files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(
|
||||
ctx.basePath,
|
||||
'_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex'
|
||||
)
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the markdown md.tex files', ctx => {
|
||||
return ctx.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(
|
||||
ctx.basePath,
|
||||
'_markdown_main/30893013dec5d869a415610079774c2f.md.tex'
|
||||
)
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should call the callback', ctx => {
|
||||
return ctx.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should time the request', ctx => {
|
||||
return ctx.Metrics.Timer.prototype.done.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_writeResourceToDisk', () => {
|
||||
describe('with a url based resource', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.fs.mkdir = sinon.stub().callsArg(2)
|
||||
ctx.resource = {
|
||||
path: 'main.tex',
|
||||
url: 'http://www.example.com/primary/main.tex',
|
||||
fallbackURL: 'http://fallback.example.com/fallback/main.tex',
|
||||
modified: Date.now(),
|
||||
}
|
||||
ctx.UrlCache.downloadUrlToFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(5, 'fake error downloading file')
|
||||
return ctx.ResourceWriter._writeResourceToDisk(
|
||||
ctx.project_id,
|
||||
ctx.resource,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure the directory exists', ctx => {
|
||||
ctx.fs.mkdir
|
||||
.calledWith(path.dirname(path.join(ctx.basePath, ctx.resource.path)))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write the URL from the cache', ctx => {
|
||||
return ctx.UrlCache.downloadUrlToFile
|
||||
.calledWith(
|
||||
ctx.project_id,
|
||||
ctx.resource.url,
|
||||
ctx.resource.fallbackURL,
|
||||
path.join(ctx.basePath, ctx.resource.path),
|
||||
ctx.resource.modified
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback', ctx => {
|
||||
return ctx.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should not return an error if the resource writer errored', ctx => {
|
||||
return expect(ctx.callback.args[0][0]).not.to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a content based resource', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.resource = {
|
||||
path: 'main.tex',
|
||||
content: 'Hello world',
|
||||
}
|
||||
ctx.fs.writeFile = sinon.stub().callsArg(2)
|
||||
ctx.fs.mkdir = sinon.stub().callsArg(2)
|
||||
return ctx.ResourceWriter._writeResourceToDisk(
|
||||
ctx.project_id,
|
||||
ctx.resource,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure the directory exists', ctx => {
|
||||
return ctx.fs.mkdir
|
||||
.calledWith(path.dirname(path.join(ctx.basePath, ctx.resource.path)))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write the contents to disk', ctx => {
|
||||
return ctx.fs.writeFile
|
||||
.calledWith(
|
||||
path.join(ctx.basePath, ctx.resource.path),
|
||||
ctx.resource.content
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', ctx => {
|
||||
return ctx.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with a file path that breaks out of the root folder', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.resource = {
|
||||
path: '../../main.tex',
|
||||
content: 'Hello world',
|
||||
}
|
||||
ctx.fs.writeFile = sinon.stub().callsArg(2)
|
||||
return ctx.ResourceWriter._writeResourceToDisk(
|
||||
ctx.project_id,
|
||||
ctx.resource,
|
||||
ctx.basePath,
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should not write to disk', ctx => {
|
||||
return ctx.fs.writeFile.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = ctx.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('checkPath', () => {
|
||||
describe('with a valid path', () => {
|
||||
beforeEach(ctx => {
|
||||
return ctx.ResourceWriter.checkPath('foo', 'bar', ctx.callback)
|
||||
})
|
||||
|
||||
return it('should return the joined path', ctx => {
|
||||
return ctx.callback.calledWith(null, 'foo/bar').should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an invalid path', () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.ResourceWriter.checkPath('foo', 'baz/../../bar', ctx.callback)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = ctx.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with another invalid path matching on a prefix', () => {
|
||||
beforeEach(ctx => {
|
||||
return ctx.ResourceWriter.checkPath(
|
||||
'foo',
|
||||
'../foobar/baz',
|
||||
ctx.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should return an error', ctx => {
|
||||
ctx.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = ctx.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,533 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/ResourceWriter'
|
||||
)
|
||||
const path = require('node:path')
|
||||
|
||||
describe('ResourceWriter', function () {
|
||||
beforeEach(function () {
|
||||
let Timer
|
||||
this.ResourceWriter = SandboxedModule.require(modulePath, {
|
||||
singleOnly: true,
|
||||
requires: {
|
||||
fs: (this.fs = {
|
||||
mkdir: sinon.stub().callsArg(1),
|
||||
unlink: sinon.stub().callsArg(1),
|
||||
}),
|
||||
'./ResourceStateManager': (this.ResourceStateManager = {}),
|
||||
'./UrlCache': (this.UrlCache = {
|
||||
createProjectDir: sinon.stub().yields(),
|
||||
}),
|
||||
'./OutputFileFinder': (this.OutputFileFinder = {}),
|
||||
'@overleaf/metrics': (this.Metrics = {
|
||||
inc: sinon.stub(),
|
||||
Timer: (Timer = (function () {
|
||||
Timer = class Timer {
|
||||
static initClass() {
|
||||
this.prototype.done = sinon.stub()
|
||||
}
|
||||
}
|
||||
Timer.initClass()
|
||||
return Timer
|
||||
})()),
|
||||
}),
|
||||
},
|
||||
})
|
||||
this.project_id = 'project-id-123'
|
||||
this.basePath = '/path/to/write/files/to'
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on a full request', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = ['resource-1-mock', 'resource-2-mock', 'resource-3-mock']
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncState: (this.syncState = '0123456789abcdef'),
|
||||
resources: this.resources,
|
||||
}
|
||||
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
|
||||
this.ResourceWriter._removeExtraneousFiles = sinon.stub().yields(null)
|
||||
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
|
||||
return this.ResourceWriter.syncResourcesToDisk(
|
||||
this.request,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should remove old files', function () {
|
||||
return this.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(this.request, this.resources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write each resource to disk', function () {
|
||||
return Array.from(this.resources).map(resource =>
|
||||
this.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(this.project_id, resource, this.basePath)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
it('should store the sync state and resource list', function () {
|
||||
return this.ResourceStateManager.saveProjectState
|
||||
.calledWith(this.syncState, this.resources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on an incremental update', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = ['resource-1-mock']
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (this.syncState = '1234567890abcdef'),
|
||||
resources: this.resources,
|
||||
}
|
||||
this.fullResources = this.resources.concat(['file-1'])
|
||||
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
|
||||
this.ResourceWriter._removeExtraneousFiles = sinon
|
||||
.stub()
|
||||
.yields(null, (this.outputFiles = []), (this.allFiles = []))
|
||||
this.ResourceStateManager.checkProjectStateMatches = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.fullResources)
|
||||
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
|
||||
this.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3)
|
||||
return this.ResourceWriter.syncResourcesToDisk(
|
||||
this.request,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should check the sync state matches', function () {
|
||||
return this.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(this.syncState, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should remove old files', function () {
|
||||
return this.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(this.request, this.fullResources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should check each resource exists', function () {
|
||||
return this.ResourceStateManager.checkResourceFiles
|
||||
.calledWith(this.fullResources, this.allFiles, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write each resource to disk', function () {
|
||||
return Array.from(this.resources).map(resource =>
|
||||
this.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(this.project_id, resource, this.basePath)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncResourcesToDisk on an incremental update when the state does not match', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = ['resource-1-mock']
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (this.syncState = '1234567890abcdef'),
|
||||
resources: this.resources,
|
||||
}
|
||||
this.ResourceStateManager.checkProjectStateMatches = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, (this.error = new Error()))
|
||||
return this.ResourceWriter.syncResourcesToDisk(
|
||||
this.request,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should check whether the sync state matches', function () {
|
||||
return this.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(this.syncState, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with an error', function () {
|
||||
return this.callback.calledWith(this.error).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_removeExtraneousFiles', function () {
|
||||
beforeEach(function () {
|
||||
this.output_files = [
|
||||
{
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'extra/file.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
{
|
||||
path: 'extra.aux',
|
||||
type: 'aux',
|
||||
},
|
||||
{
|
||||
path: 'cache/_chunk1',
|
||||
},
|
||||
{
|
||||
path: 'figures/image-eps-converted-to.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.md5',
|
||||
type: 'md5',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.dpth',
|
||||
type: 'dpth',
|
||||
},
|
||||
{
|
||||
path: 'foo/main-figure0.pdf',
|
||||
type: 'pdf',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/default-pyg-prefix.pygstyle',
|
||||
type: 'pygstyle',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/default.pygstyle',
|
||||
type: 'pygstyle',
|
||||
},
|
||||
{
|
||||
path: '_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex',
|
||||
type: 'pygtex',
|
||||
},
|
||||
{
|
||||
path: '_markdown_main/30893013dec5d869a415610079774c2f.md.tex',
|
||||
type: 'tex',
|
||||
},
|
||||
{
|
||||
path: 'output.stdout',
|
||||
},
|
||||
{
|
||||
path: 'output.stderr',
|
||||
},
|
||||
]
|
||||
this.resources = 'mock-resources'
|
||||
this.request = {
|
||||
project_id: this.project_id,
|
||||
syncType: 'incremental',
|
||||
syncState: (this.syncState = '1234567890abcdef'),
|
||||
resources: this.resources,
|
||||
metricsOpts: { path: 'foo' },
|
||||
}
|
||||
this.OutputFileFinder.findOutputFiles = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.output_files)
|
||||
this.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1)
|
||||
return this.ResourceWriter._removeExtraneousFiles(
|
||||
this.request,
|
||||
this.resources,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should find the existing output files', function () {
|
||||
return this.OutputFileFinder.findOutputFiles
|
||||
.calledWith(this.resources, this.basePath)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the output files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'output.pdf'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the stdout log file', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'output.stdout'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the stderr log file', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'output.stderr'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should delete the extra files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'extra/file.tex'))
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should not delete the extra aux files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'extra.aux'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the knitr cache file', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'cache/_chunk1'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the epstopdf converted files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(this.basePath, 'figures/image-eps-converted-to.pdf')
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz md5 files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'foo/main-figure0.md5'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz dpth files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'foo/main-figure0.dpth'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the tikz pdf files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, 'foo/main-figure0.pdf'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted pygstyle files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(this.basePath, '_minted-main/default-pyg-prefix.pygstyle')
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted default pygstyle files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, '_minted-main/default.pygstyle'))
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the minted default pygtex files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(
|
||||
this.basePath,
|
||||
'_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex'
|
||||
)
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not delete the markdown md.tex files', function () {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(
|
||||
path.join(
|
||||
this.basePath,
|
||||
'_markdown_main/30893013dec5d869a415610079774c2f.md.tex'
|
||||
)
|
||||
)
|
||||
.should.equal(false)
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should time the request', function () {
|
||||
return this.Metrics.Timer.prototype.done.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_writeResourceToDisk', function () {
|
||||
describe('with a url based resource', function () {
|
||||
beforeEach(function () {
|
||||
this.fs.mkdir = sinon.stub().callsArg(2)
|
||||
this.resource = {
|
||||
path: 'main.tex',
|
||||
url: 'http://www.example.com/primary/main.tex',
|
||||
fallbackURL: 'http://fallback.example.com/fallback/main.tex',
|
||||
modified: Date.now(),
|
||||
}
|
||||
this.UrlCache.downloadUrlToFile = sinon
|
||||
.stub()
|
||||
.callsArgWith(5, 'fake error downloading file')
|
||||
return this.ResourceWriter._writeResourceToDisk(
|
||||
this.project_id,
|
||||
this.resource,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure the directory exists', function () {
|
||||
this.fs.mkdir
|
||||
.calledWith(
|
||||
path.dirname(path.join(this.basePath, this.resource.path))
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write the URL from the cache', function () {
|
||||
return this.UrlCache.downloadUrlToFile
|
||||
.calledWith(
|
||||
this.project_id,
|
||||
this.resource.url,
|
||||
this.resource.fallbackURL,
|
||||
path.join(this.basePath, this.resource.path),
|
||||
this.resource.modified
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should not return an error if the resource writer errored', function () {
|
||||
return expect(this.callback.args[0][0]).not.to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a content based resource', function () {
|
||||
beforeEach(function () {
|
||||
this.resource = {
|
||||
path: 'main.tex',
|
||||
content: 'Hello world',
|
||||
}
|
||||
this.fs.writeFile = sinon.stub().callsArg(2)
|
||||
this.fs.mkdir = sinon.stub().callsArg(2)
|
||||
return this.ResourceWriter._writeResourceToDisk(
|
||||
this.project_id,
|
||||
this.resource,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should ensure the directory exists', function () {
|
||||
return this.fs.mkdir
|
||||
.calledWith(
|
||||
path.dirname(path.join(this.basePath, this.resource.path))
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write the contents to disk', function () {
|
||||
return this.fs.writeFile
|
||||
.calledWith(
|
||||
path.join(this.basePath, this.resource.path),
|
||||
this.resource.content
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with a file path that breaks out of the root folder', function () {
|
||||
beforeEach(function () {
|
||||
this.resource = {
|
||||
path: '../../main.tex',
|
||||
content: 'Hello world',
|
||||
}
|
||||
this.fs.writeFile = sinon.stub().callsArg(2)
|
||||
return this.ResourceWriter._writeResourceToDisk(
|
||||
this.project_id,
|
||||
this.resource,
|
||||
this.basePath,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should not write to disk', function () {
|
||||
return this.fs.writeFile.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('checkPath', function () {
|
||||
describe('with a valid path', function () {
|
||||
beforeEach(function () {
|
||||
return this.ResourceWriter.checkPath('foo', 'bar', this.callback)
|
||||
})
|
||||
|
||||
return it('should return the joined path', function () {
|
||||
return this.callback.calledWith(null, 'foo/bar').should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an invalid path', function () {
|
||||
beforeEach(function () {
|
||||
this.ResourceWriter.checkPath('foo', 'baz/../../bar', this.callback)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with another invalid path matching on a prefix', function () {
|
||||
beforeEach(function () {
|
||||
return this.ResourceWriter.checkPath(
|
||||
'foo',
|
||||
'../foobar/baz',
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback.calledWith(sinon.match(Error)).should.equal(true)
|
||||
|
||||
const message = this.callback.args[0][0].message
|
||||
expect(message).to.include('resource path is outside root directory')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,270 @@
|
||||
import { vi, describe, beforeEach, it } from 'vitest'
|
||||
|
||||
import path from 'node:path'
|
||||
import sinon from 'sinon'
|
||||
const modulePath = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/StaticServerForbidSymlinks'
|
||||
)
|
||||
|
||||
describe('StaticServerForbidSymlinks', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.settings = {
|
||||
path: {
|
||||
compilesDir: '/compiles/here',
|
||||
},
|
||||
}
|
||||
|
||||
ctx.fs = {}
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: ctx.settings,
|
||||
}))
|
||||
|
||||
vi.doMock('fs', () => ({
|
||||
default: ctx.fs,
|
||||
}))
|
||||
|
||||
ctx.ForbidSymlinks = (await import(modulePath)).default
|
||||
|
||||
ctx.dummyStatic = (rootDir, options) => (req, res, next) =>
|
||||
// console.log "dummyStatic serving file", rootDir, "called with", req.url
|
||||
// serve it
|
||||
next()
|
||||
|
||||
ctx.StaticServerForbidSymlinks = ctx.ForbidSymlinks(
|
||||
ctx.dummyStatic,
|
||||
ctx.settings.path.compilesDir
|
||||
)
|
||||
ctx.req = {
|
||||
params: {
|
||||
project_id: '12345',
|
||||
},
|
||||
}
|
||||
|
||||
ctx.res = {}
|
||||
ctx.req.url = '/12345/output.pdf'
|
||||
})
|
||||
|
||||
describe('sending a normal file through', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
null,
|
||||
`${ctx.settings.path.compilesDir}/${ctx.req.params.project_id}/output.pdf`
|
||||
)
|
||||
})
|
||||
|
||||
it('should call next', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(200)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res, err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a missing file', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
{ code: 'ENOENT' },
|
||||
`${ctx.settings.path.compilesDir}/${ctx.req.params.project_id}/unknown.pdf`
|
||||
)
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a new line', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = '/12345/output.pdf\nother file'
|
||||
ctx.fs.realpath = sinon.stub().yields()
|
||||
})
|
||||
|
||||
it('should process the correct file', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = () => {
|
||||
ctx.fs.realpath.should.have.been.calledWith(
|
||||
`${ctx.settings.path.compilesDir}/12345/output.pdf\nother file`
|
||||
)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a symlink file', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, `/etc/${ctx.req.params.project_id}/output.pdf`)
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a relative file', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = '/12345/../67890/output.pdf'
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a unnormalized file containing .', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = '/12345/foo/./output.pdf'
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file containing an empty path', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = '/12345/foo//output.pdf'
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a non-project file', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = '/.foo/output.pdf'
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file outside the compiledir', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = '/../bar/output.pdf'
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file with no leading /', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = './../bar/output.pdf'
|
||||
})
|
||||
|
||||
it('should send a 404', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a github style path', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.url = '/henryoswald-latex_example/output/output.log'
|
||||
ctx.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
null,
|
||||
`${ctx.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`
|
||||
)
|
||||
})
|
||||
|
||||
it('should call next', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(200)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res, err => {
|
||||
if (err) reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an error from fs.realpath', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.fs.realpath = sinon.stub().callsArgWith(1, 'error')
|
||||
})
|
||||
|
||||
it('should send a 500', async function (ctx) {
|
||||
await new Promise((resolve, reject) => {
|
||||
ctx.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(500)
|
||||
resolve()
|
||||
}
|
||||
ctx.StaticServerForbidSymlinks(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,248 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const assert = require('node:assert')
|
||||
const path = require('node:path')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = path.join(
|
||||
__dirname,
|
||||
'../../../app/js/StaticServerForbidSymlinks'
|
||||
)
|
||||
const { expect } = require('chai')
|
||||
|
||||
describe('StaticServerForbidSymlinks', function () {
|
||||
beforeEach(function () {
|
||||
this.settings = {
|
||||
path: {
|
||||
compilesDir: '/compiles/here',
|
||||
},
|
||||
}
|
||||
|
||||
this.fs = {}
|
||||
this.ForbidSymlinks = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.settings,
|
||||
fs: this.fs,
|
||||
},
|
||||
})
|
||||
|
||||
this.dummyStatic = (rootDir, options) => (req, res, next) =>
|
||||
// console.log "dummyStatic serving file", rootDir, "called with", req.url
|
||||
// serve it
|
||||
next()
|
||||
|
||||
this.StaticServerForbidSymlinks = this.ForbidSymlinks(
|
||||
this.dummyStatic,
|
||||
this.settings.path.compilesDir
|
||||
)
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: '12345',
|
||||
},
|
||||
}
|
||||
|
||||
this.res = {}
|
||||
return (this.req.url = '/12345/output.pdf')
|
||||
})
|
||||
|
||||
describe('sending a normal file through', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
null,
|
||||
`${this.settings.path.compilesDir}/${this.req.params.project_id}/output.pdf`
|
||||
))
|
||||
})
|
||||
|
||||
return it('should call next', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(200)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res, done)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a missing file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
{ code: 'ENOENT' },
|
||||
`${this.settings.path.compilesDir}/${this.req.params.project_id}/unknown.pdf`
|
||||
))
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a new line', function () {
|
||||
beforeEach(function () {
|
||||
this.req.url = '/12345/output.pdf\nother file'
|
||||
this.fs.realpath = sinon.stub().yields()
|
||||
})
|
||||
|
||||
it('should process the correct file', function (done) {
|
||||
this.res.sendStatus = () => {
|
||||
this.fs.realpath.should.have.been.calledWith(
|
||||
`${this.settings.path.compilesDir}/12345/output.pdf\nother file`
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a symlink file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`))
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a relative file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/12345/../67890/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a unnormalized file containing .', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/12345/foo/./output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file containing an empty path', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/12345/foo//output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a non-project file', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/.foo/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file outside the compiledir', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = '/../bar/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a file with no leading /', function () {
|
||||
beforeEach(function () {
|
||||
return (this.req.url = './../bar/output.pdf')
|
||||
})
|
||||
|
||||
return it('should send a 404', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(404)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a github style path', function () {
|
||||
beforeEach(function () {
|
||||
this.req.url = '/henryoswald-latex_example/output/output.log'
|
||||
return (this.fs.realpath = sinon
|
||||
.stub()
|
||||
.callsArgWith(
|
||||
1,
|
||||
null,
|
||||
`${this.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`
|
||||
))
|
||||
})
|
||||
|
||||
return it('should call next', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(200)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res, done)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with an error from fs.realpath', function () {
|
||||
beforeEach(function () {
|
||||
return (this.fs.realpath = sinon.stub().callsArgWith(1, 'error'))
|
||||
})
|
||||
|
||||
return it('should send a 500', function (done) {
|
||||
this.res.sendStatus = function (resCode) {
|
||||
resCode.should.equal(500)
|
||||
return done()
|
||||
}
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
+13
-2
@@ -1,5 +1,16 @@
|
||||
const { expect } = require('chai')
|
||||
const { sampleByHash, sampleRequest } = require('../../../app/js/StatsManager')
|
||||
import { expect, describe, it, vi } from 'vitest'
|
||||
import StatsManager from '../../../app/js/StatsManager.js'
|
||||
|
||||
const { sampleByHash, sampleRequest } = StatsManager
|
||||
|
||||
// Mocks allow us to import Metrics.js twice without getting errors.
|
||||
vi.mock('@overleaf/metrics', () => ({
|
||||
prom: {
|
||||
Gauge: vi.fn(),
|
||||
Counter: vi.fn(),
|
||||
Histogram: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('StatsManager', function () {
|
||||
describe('sampleByHash', function () {
|
||||
+18
-16
@@ -1,16 +1,18 @@
|
||||
const Path = require('node:path')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { expect } = require('chai')
|
||||
import Path from 'node:path'
|
||||
import { expect, describe, beforeEach, it } from 'vitest'
|
||||
|
||||
const MODULE_PATH = Path.join(__dirname, '../../../app/js/SynctexOutputParser')
|
||||
const MODULE_PATH = Path.join(
|
||||
import.meta.dirname,
|
||||
'../../../app/js/SynctexOutputParser'
|
||||
)
|
||||
|
||||
describe('SynctexOutputParser', function () {
|
||||
beforeEach(function () {
|
||||
this.SynctexOutputParser = SandboxedModule.require(MODULE_PATH)
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.SynctexOutputParser = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
describe('parseViewOutput', function () {
|
||||
it('parses valid output', function () {
|
||||
it('parses valid output', function (ctx) {
|
||||
const output = `This is SyncTeX command line utility, version 1.5
|
||||
SyncTeX result begin
|
||||
Output:/compile/output.pdf
|
||||
@@ -39,7 +41,7 @@ middle:
|
||||
after:
|
||||
SyncTeX result end
|
||||
`
|
||||
const records = this.SynctexOutputParser.parseViewOutput(output)
|
||||
const records = ctx.SynctexOutputParser.parseViewOutput(output)
|
||||
expect(records).to.deep.equal([
|
||||
{
|
||||
page: 1,
|
||||
@@ -58,15 +60,15 @@ SyncTeX result end
|
||||
])
|
||||
})
|
||||
|
||||
it('handles garbage', function () {
|
||||
it('handles garbage', function (ctx) {
|
||||
const output = 'This computer is on strike!'
|
||||
const records = this.SynctexOutputParser.parseViewOutput(output)
|
||||
const records = ctx.SynctexOutputParser.parseViewOutput(output)
|
||||
expect(records).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseEditOutput', function () {
|
||||
it('parses valid output', function () {
|
||||
it('parses valid output', function (ctx) {
|
||||
const output = `This is SyncTeX command line utility, version 1.5
|
||||
SyncTeX result begin
|
||||
Output:/compile/output.pdf
|
||||
@@ -77,7 +79,7 @@ Offset:0
|
||||
Context:
|
||||
SyncTeX result end
|
||||
`
|
||||
const records = this.SynctexOutputParser.parseEditOutput(
|
||||
const records = ctx.SynctexOutputParser.parseEditOutput(
|
||||
output,
|
||||
'/compile'
|
||||
)
|
||||
@@ -86,7 +88,7 @@ SyncTeX result end
|
||||
])
|
||||
})
|
||||
|
||||
it('handles values that contain colons', function () {
|
||||
it('handles values that contain colons', function (ctx) {
|
||||
const output = `This is SyncTeX command line utility, version 1.5
|
||||
SyncTeX result begin
|
||||
Output:/compile/output.pdf
|
||||
@@ -98,7 +100,7 @@ Context:
|
||||
SyncTeX result end
|
||||
`
|
||||
|
||||
const records = this.SynctexOutputParser.parseEditOutput(
|
||||
const records = ctx.SynctexOutputParser.parseEditOutput(
|
||||
output,
|
||||
'/compile'
|
||||
)
|
||||
@@ -107,9 +109,9 @@ SyncTeX result end
|
||||
])
|
||||
})
|
||||
|
||||
it('handles garbage', function () {
|
||||
it('handles garbage', function (ctx) {
|
||||
const output = '2 + 2 = 4'
|
||||
const records = this.SynctexOutputParser.parseEditOutput(output)
|
||||
const records = ctx.SynctexOutputParser.parseEditOutput(output)
|
||||
expect(records).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
@@ -1,185 +0,0 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = require('node:path').join(
|
||||
__dirname,
|
||||
'../../../app/js/TikzManager'
|
||||
)
|
||||
|
||||
describe('TikzManager', function () {
|
||||
beforeEach(function () {
|
||||
return (this.TikzManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./ResourceWriter': (this.ResourceWriter = {}),
|
||||
'./SafeReader': (this.SafeReader = {}),
|
||||
fs: (this.fs = {}),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('checkMainFile', function () {
|
||||
beforeEach(function () {
|
||||
this.compileDir = 'compile-dir'
|
||||
this.mainFile = 'main.tex'
|
||||
return (this.callback = sinon.stub())
|
||||
})
|
||||
|
||||
describe('if there is already an output.tex file in the resources', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = [{ path: 'main.tex' }, { path: 'output.tex' }]
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
return it('should call the callback with false ', function () {
|
||||
return this.callback.calledWithExactly(null, false).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('if there is no output.tex file in the resources', function () {
|
||||
beforeEach(function () {
|
||||
this.resources = [{ path: 'main.tex' }]
|
||||
return (this.ResourceWriter.checkPath = sinon
|
||||
.stub()
|
||||
.withArgs(this.compileDir, this.mainFile)
|
||||
.callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`))
|
||||
})
|
||||
|
||||
describe('and the main file contains tikzexternalize', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, 'hello \\tikzexternalize')
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should look at the file on disk', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with true ', function () {
|
||||
return this.callback.calledWithExactly(null, true).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('and the main file does not contain tikzexternalize', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, 'hello')
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should look at the file on disk', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with false', function () {
|
||||
return this.callback.calledWithExactly(null, false).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('and the main file contains \\usepackage{pstool}', function () {
|
||||
beforeEach(function () {
|
||||
this.SafeReader.readFile = sinon
|
||||
.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, 'hello \\usepackage[random-options]{pstool}')
|
||||
return this.TikzManager.checkMainFile(
|
||||
this.compileDir,
|
||||
this.mainFile,
|
||||
this.resources,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should look at the file on disk', function () {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback with true ', function () {
|
||||
return this.callback.calledWithExactly(null, true).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('injectOutputFile', function () {
|
||||
beforeEach(function () {
|
||||
this.rootDir = '/mock'
|
||||
this.filename = 'filename.tex'
|
||||
this.callback = sinon.stub()
|
||||
this.content = `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{tikz}
|
||||
\\tikzexternalize
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`
|
||||
this.fs.readFile = sinon.stub().callsArgWith(2, null, this.content)
|
||||
this.fs.writeFile = sinon.stub().callsArg(3)
|
||||
this.ResourceWriter.checkPath = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, null, `${this.rootDir}/${this.filename}`)
|
||||
return this.TikzManager.injectOutputFile(
|
||||
this.rootDir,
|
||||
this.filename,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('sould check the path', function () {
|
||||
return this.ResourceWriter.checkPath
|
||||
.calledWith(this.rootDir, this.filename)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should read the file', function () {
|
||||
return this.fs.readFile
|
||||
.calledWith(`${this.rootDir}/${this.filename}`, 'utf8')
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should write out the same file as output.tex', function () {
|
||||
return this.fs.writeFile
|
||||
.calledWith(`${this.rootDir}/output.tex`, this.content, { flag: 'wx' })
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should call the callback', function () {
|
||||
return this.callback.called.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user