ci: fix all tests on windows
This commit is contained in:
Родитель
84817dc151
Коммит
39d4722adc
|
@ -13,5 +13,6 @@ src
|
|||
scripts
|
||||
cypress*
|
||||
*.spec.*
|
||||
jest.helpers.*
|
||||
/docs
|
||||
/e2e
|
||||
|
|
|
@ -4,7 +4,9 @@ describe("framework detection", () => {
|
|||
it("should detect frameworks", async () => {
|
||||
process.chdir(__dirname);
|
||||
|
||||
const result = await detect("./samples", 2);
|
||||
let result = await detect("./samples", 2);
|
||||
// Fix windows paths
|
||||
result = result.replace(/\\/g, "/");
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"Detected api folders (6):
|
||||
- samples/api/dotnet (.NET)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import mockFs from "mock-fs";
|
||||
import { build } from "./build";
|
||||
import { DEFAULT_CONFIG } from "../../config";
|
||||
import { convertToNativePaths } from "../../jest.helpers.";
|
||||
|
||||
jest.mock("child_process", () => ({
|
||||
execSync: jest.fn(),
|
||||
|
@ -32,11 +33,11 @@ describe("swa build", () => {
|
|||
|
||||
it("should run command in package.json path", async () => {
|
||||
const execSyncMock = jest.requireMock("child_process").execSync;
|
||||
mockFs({ "app/package.json": {} });
|
||||
mockFs({ [convertToNativePaths("app/package.json")]: {} });
|
||||
|
||||
await build({
|
||||
...DEFAULT_CONFIG,
|
||||
outputLocation: "app/dist",
|
||||
outputLocation: convertToNativePaths("app/dist"),
|
||||
appBuildCommand: "npm run something",
|
||||
});
|
||||
expect(execSyncMock.mock.calls[0][1].cwd).toBe("app");
|
||||
|
@ -46,7 +47,7 @@ describe("swa build", () => {
|
|||
const execSyncMock = jest.requireMock("child_process").execSync;
|
||||
mockFs();
|
||||
|
||||
await build({ ...DEFAULT_CONFIG, apiLocation: "api/", apiBuildCommand: "npm run something" });
|
||||
await build({ ...DEFAULT_CONFIG, apiLocation: "api", apiBuildCommand: "npm run something" });
|
||||
expect(execSyncMock.mock.calls[0][0]).toBe("npm run something");
|
||||
});
|
||||
|
||||
|
@ -54,14 +55,14 @@ describe("swa build", () => {
|
|||
const execSyncMock = jest.requireMock("child_process").execSync;
|
||||
mockFs({ "api/package.json": {} });
|
||||
|
||||
await build({ ...DEFAULT_CONFIG, apiLocation: "api/", apiBuildCommand: "npm run something" });
|
||||
await build({ ...DEFAULT_CONFIG, apiLocation: "api", apiBuildCommand: "npm run something" });
|
||||
expect(execSyncMock.mock.calls[0][0]).toBe("npm install");
|
||||
expect(execSyncMock.mock.calls[1][0]).toBe("npm run something");
|
||||
});
|
||||
|
||||
it("should run command in package.json path", async () => {
|
||||
const execSyncMock = jest.requireMock("child_process").execSync;
|
||||
mockFs({ "api/package.json": {} });
|
||||
mockFs({ [convertToNativePaths("api/package.json")]: {} });
|
||||
|
||||
await build({
|
||||
...DEFAULT_CONFIG,
|
||||
|
@ -85,8 +86,8 @@ describe("swa build", () => {
|
|||
|
||||
await build({ ...DEFAULT_CONFIG, auto: true });
|
||||
expect(execSyncMock.mock.calls[0][0]).toBe("npm install");
|
||||
expect(execSyncMock.mock.calls[0][1].cwd).toBe("src/node-ts");
|
||||
expect(execSyncMock.mock.calls[0][1].cwd).toBe(convertToNativePaths("src/node-ts"));
|
||||
expect(execSyncMock.mock.calls[1][0]).toBe("npm run build --if-present");
|
||||
expect(execSyncMock.mock.calls[1][1].cwd).toBe("src/node-ts");
|
||||
expect(execSyncMock.mock.calls[1][1].cwd).toBe(convertToNativePaths("src/node-ts"));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import mockFs from "mock-fs";
|
|||
import { init } from "./init";
|
||||
import { DEFAULT_CONFIG } from "../../config";
|
||||
import { swaCliConfigFilename } from "../../core/utils";
|
||||
import { convertToNativePaths, convertToUnixPaths } from "../../jest.helpers.";
|
||||
|
||||
jest.mock("prompts", () => jest.fn());
|
||||
|
||||
|
@ -13,9 +14,9 @@ const defaultCliConfig = {
|
|||
|
||||
const defautResolvedPrompts = {
|
||||
projectName: "test-project",
|
||||
appLocation: "./app",
|
||||
apiLocation: "./api",
|
||||
outputLocation: "./dist",
|
||||
appLocation: convertToNativePaths("./app"),
|
||||
apiLocation: convertToNativePaths("./api"),
|
||||
outputLocation: convertToNativePaths("./dist"),
|
||||
appBuildCommand: "npm run build",
|
||||
apiBuildCommand: "npm run build:api",
|
||||
devServerCommand: "npm run dev",
|
||||
|
@ -106,14 +107,14 @@ describe("swa init", () => {
|
|||
const configJson = JSON.parse(fs.readFileSync(defaultCliConfig.config, "utf-8"));
|
||||
const lastCall = promptsMock.mock.calls.length - 1;
|
||||
expect(promptsMock.mock.calls[lastCall][0].name).toEqual("confirmOverwrite");
|
||||
expect(configJson.configurations.test.outputLocation).toEqual("./dist");
|
||||
expect(configJson.configurations.test.outputLocation).toEqual(convertToNativePaths("./dist"));
|
||||
});
|
||||
|
||||
it("should detect frameworks and create a config file", async () => {
|
||||
mockFs({ src: mockFs.load("e2e/fixtures/static-node-ts") });
|
||||
|
||||
await init({ ...defaultCliConfig, configName: "test", yes: true });
|
||||
const configFile = fs.readFileSync(defaultCliConfig.config, "utf-8");
|
||||
const configFile = convertToUnixPaths(fs.readFileSync(defaultCliConfig.config, "utf-8"));
|
||||
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"{
|
||||
|
@ -134,7 +135,7 @@ describe("swa init", () => {
|
|||
mockFs({ src: mockFs.load("e2e/fixtures/astro-node") });
|
||||
|
||||
await init({ ...defaultCliConfig, configName: "test", yes: true });
|
||||
const configFile = fs.readFileSync(defaultCliConfig.config, "utf-8");
|
||||
const configFile = convertToUnixPaths(fs.readFileSync(defaultCliConfig.config, "utf-8"));
|
||||
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"{
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { convertToUnixPaths } from "../../jest.helpers.";
|
||||
import { detectProjectFolders, formatDetectedFolders, generateConfiguration } from "./detect";
|
||||
|
||||
describe("detectProjectFolders()", () => {
|
||||
it("should detect frameworks", async () => {
|
||||
const detectedFolders = await detectProjectFolders("e2e/fixtures");
|
||||
const detectedFolders = convertToUnixPaths(await detectProjectFolders("e2e/fixtures"));
|
||||
expect(detectedFolders.api.length).toBe(2);
|
||||
expect(detectedFolders.app.length).toBe(2);
|
||||
expect(formatDetectedFolders(detectedFolders.api, "api")).toMatchInlineSnapshot(`
|
||||
|
@ -21,7 +22,7 @@ describe("detectProjectFolders()", () => {
|
|||
describe("generateConfiguration()", () => {
|
||||
it("should generate expected configuration for app astro-node", async () => {
|
||||
const { app, api } = await detectProjectFolders("e2e/fixtures/astro-node");
|
||||
const config = await generateConfiguration(app[0], api[0]);
|
||||
const config = convertToUnixPaths(await generateConfiguration(app[0], api[0]));
|
||||
expect(config).toEqual({
|
||||
apiBuildCommand: "npm run build --if-present",
|
||||
apiLocation: "e2e/fixtures/astro-node/node",
|
||||
|
@ -36,7 +37,7 @@ describe("generateConfiguration()", () => {
|
|||
|
||||
it("should generate expected configuration for app static-node-ts", async () => {
|
||||
const { app, api } = await detectProjectFolders("e2e/fixtures/static-node-ts");
|
||||
const config = await generateConfiguration(app[0], api[0]);
|
||||
const config = convertToUnixPaths(await generateConfiguration(app[0], api[0]));
|
||||
expect(config).toEqual({
|
||||
apiBuildCommand: "npm run build --if-present",
|
||||
apiLocation: "e2e/fixtures/static-node-ts/node-ts",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import mockFs from "mock-fs";
|
||||
import { convertToNativePaths } from "../../jest.helpers.";
|
||||
import { findUpPackageJsonDir, pathExists, safeReadFile, safeReadJson } from "./file";
|
||||
|
||||
describe("safeReadJson()", () => {
|
||||
|
@ -73,17 +74,17 @@ describe("findUpPackageJsonDir()", () => {
|
|||
});
|
||||
|
||||
it("should return base path", async () => {
|
||||
mockFs({ "app/package.json": "{}" });
|
||||
expect(await findUpPackageJsonDir("app/", "dist")).toBe("app");
|
||||
mockFs({ [convertToNativePaths("app/package.json")]: "{}" });
|
||||
expect(await findUpPackageJsonDir(convertToNativePaths("app/"), "dist")).toBe("app");
|
||||
});
|
||||
|
||||
it("should return start path", async () => {
|
||||
mockFs({ "app/dist/package.json": "{}" });
|
||||
expect(await findUpPackageJsonDir("app", "dist/")).toBe("app/dist");
|
||||
mockFs({ [convertToNativePaths("app/dist/package.json")]: "{}" });
|
||||
expect(await findUpPackageJsonDir("app", convertToNativePaths("dist/"))).toBe(convertToNativePaths("app/dist"));
|
||||
});
|
||||
|
||||
it("should return the correct path", async () => {
|
||||
mockFs({ "app/toto/package.json": "{}" });
|
||||
expect(await findUpPackageJsonDir("app", "toto/dist")).toBe("app/toto");
|
||||
mockFs({ [convertToNativePaths("app/toto/package.json")]: "{}" });
|
||||
expect(await findUpPackageJsonDir("app", convertToNativePaths("toto/dist"))).toBe(convertToNativePaths("app/toto"));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
jest.mock("../constants", () => {});
|
||||
import mockFs from "mock-fs";
|
||||
import path from "path";
|
||||
import { convertToNativePaths } from "../../jest.helpers.";
|
||||
import { readWorkflowFile } from "./workflow-config";
|
||||
|
||||
jest.mock("../../config", () => {
|
||||
return {
|
||||
DEFAULT_CONFIG: {
|
||||
appLocation: "/",
|
||||
outputLocation: "/baz",
|
||||
appLocation: convertToNativePaths("/"),
|
||||
outputLocation: convertToNativePaths("/baz"),
|
||||
appBuildCommand: "npm run foobar",
|
||||
apiBuildCommand: "npm run foobar",
|
||||
},
|
||||
|
@ -18,7 +19,7 @@ describe("readWorkflowFile()", () => {
|
|||
let processSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
processSpy = jest.spyOn(process, "cwd").mockReturnValue("/ABSOLUTE_PATH");
|
||||
processSpy = jest.spyOn(process, "cwd").mockReturnValue(convertToNativePaths("/ABSOLUTE_PATH"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -32,7 +33,7 @@ describe("readWorkflowFile()", () => {
|
|||
|
||||
it("config file with wrong filename should return undefined", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/wrong-file-name-pattern.yml": "",
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/wrong-file-name-pattern.yml")]: "",
|
||||
});
|
||||
|
||||
expect(readWorkflowFile()).toBe(undefined);
|
||||
|
@ -40,7 +41,7 @@ describe("readWorkflowFile()", () => {
|
|||
|
||||
it("invalid YAML file should throw", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps__not-valid.yml": "",
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps__not-valid.yml")]: "",
|
||||
});
|
||||
|
||||
expect(() => readWorkflowFile()).toThrow(/could not parse the SWA workflow file/);
|
||||
|
@ -49,7 +50,7 @@ describe("readWorkflowFile()", () => {
|
|||
describe("checking workflow properties", () => {
|
||||
it(`missing property "jobs" should throw`, () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps__not-valid.yml": `name: Azure Static Web Apps CI/CD`,
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps__not-valid.yml")]: `name: Azure Static Web Apps CI/CD`,
|
||||
});
|
||||
|
||||
expect(() => readWorkflowFile()).toThrow(/missing property "jobs"/);
|
||||
|
@ -57,7 +58,7 @@ describe("readWorkflowFile()", () => {
|
|||
|
||||
it(`missing property "jobs.build_and_deploy_job" should throw`, () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
invalid_property:
|
||||
`,
|
||||
|
@ -67,7 +68,7 @@ jobs:
|
|||
|
||||
it(`missing property "jobs.build_and_deploy_job.steps" should throw`, () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
invalid_property:
|
||||
|
@ -79,7 +80,7 @@ jobs:
|
|||
|
||||
it(`invalid property"jobs.build_and_deploy_job.steps" should throw`, () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -90,7 +91,7 @@ jobs:
|
|||
|
||||
it(`invalid property "jobs.build_and_deploy_job.steps[]" should throw`, () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -103,7 +104,7 @@ jobs:
|
|||
|
||||
it(`missing property "jobs.build_and_deploy_job.steps[].with" should throw`, () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -119,7 +120,7 @@ jobs:
|
|||
describe("checking SWA properties", () => {
|
||||
it("property 'app_location' should be set", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -133,12 +134,12 @@ jobs:
|
|||
const workflow = readWorkflowFile();
|
||||
|
||||
expect(workflow).toBeTruthy();
|
||||
expect(workflow?.appLocation).toBe(path.normalize("/ABSOLUTE_PATH/"));
|
||||
expect(workflow?.appLocation).toBe(path.normalize(convertToNativePaths("/ABSOLUTE_PATH/")));
|
||||
});
|
||||
|
||||
it("property 'app_location' should be set to '/' if missing", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -152,12 +153,12 @@ jobs:
|
|||
const workflow = readWorkflowFile();
|
||||
|
||||
expect(workflow).toBeTruthy();
|
||||
expect(workflow?.appLocation).toBe("/ABSOLUTE_PATH/");
|
||||
expect(workflow?.appLocation).toBe(convertToNativePaths("/ABSOLUTE_PATH/"));
|
||||
});
|
||||
|
||||
it("property 'api_location' should be set", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -171,12 +172,12 @@ jobs:
|
|||
const workflow = readWorkflowFile();
|
||||
|
||||
expect(workflow).toBeTruthy();
|
||||
expect(workflow?.apiLocation).toBe(path.normalize("/ABSOLUTE_PATH/api"));
|
||||
expect(workflow?.apiLocation).toBe(path.normalize(convertToNativePaths("/ABSOLUTE_PATH/api")));
|
||||
});
|
||||
|
||||
it("property 'api_location' should be undefined if missing", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -195,7 +196,7 @@ jobs:
|
|||
|
||||
it("property 'output_location' should be set", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -209,12 +210,12 @@ jobs:
|
|||
const workflow = readWorkflowFile();
|
||||
|
||||
expect(workflow).toBeTruthy();
|
||||
expect(workflow?.outputLocation).toBe("/ABSOLUTE_PATH/");
|
||||
expect(workflow?.outputLocation).toBe(convertToNativePaths("/ABSOLUTE_PATH/"));
|
||||
});
|
||||
|
||||
it("property 'output_location' should be set to default if missing", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -228,12 +229,12 @@ jobs:
|
|||
const workflow = readWorkflowFile();
|
||||
|
||||
expect(workflow).toBeTruthy();
|
||||
expect(workflow?.outputLocation).toBe("/ABSOLUTE_PATH/baz");
|
||||
expect(workflow?.outputLocation).toBe(convertToNativePaths("/ABSOLUTE_PATH/baz"));
|
||||
});
|
||||
|
||||
it("property 'app_build_command' should be set", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -252,7 +253,7 @@ jobs:
|
|||
|
||||
it("property 'app_build_command' should be set to default if missing", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -271,7 +272,7 @@ jobs:
|
|||
|
||||
it("property 'api_build_command' should be set", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
@ -290,7 +291,7 @@ jobs:
|
|||
|
||||
it("property 'api_build_command' should be set to default if missing", () => {
|
||||
mockFs({
|
||||
"/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml": `
|
||||
[convertToNativePaths("/ABSOLUTE_PATH/.github/workflows/azure-static-web-apps.yml")]: `
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
steps:
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const isWindows = /win/.test(os.platform());
|
||||
|
||||
// Deep convert any Windows path to Unix path in a string or object
|
||||
export function convertToUnixPaths<T>(obj: T): T {
|
||||
if (!isWindows) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === "string") {
|
||||
return obj.replace(/\\\\?/g, "/") as any as T;
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map((value) => convertToUnixPaths(value)) as any as T;
|
||||
} else if (typeof obj === "object") {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
(obj as any)[k] = convertToUnixPaths(v);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Deep convert unix paths to native path in a string or object
|
||||
export function convertToNativePaths<T>(obj: T): T {
|
||||
if (!isWindows) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === "string") {
|
||||
return /^https?:\/\//.test(obj) ? obj : (obj.replace(/\//g, path.sep) as any as T);
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map((value) => convertToNativePaths(value)) as any as T;
|
||||
} else if (typeof obj === "object") {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
(obj as any)[k] = convertToNativePaths(v);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
Загрузка…
Ссылка в новой задаче