From ea3400aca64be04e221e69324c19eca3b025289d Mon Sep 17 00:00:00 2001 From: Hexxa Date: Sat, 30 Jan 2021 10:01:38 +0800 Subject: [PATCH] Add tests for client (#33) * fix(fs/local): force closing fds and add backoffs, unit tests * test(client/web): add unit tests --- .../src/components/__test__/browser.test.tsx | 95 +++++++++++++++++-- .../src/components/__test__/panes.test.tsx | 49 ++++++++++ .../components/__test__/root_frame.test.tsx | 52 ++++++++++ src/client/web/src/components/browser.tsx | 25 +++-- src/client/web/src/components/core_state.ts | 43 ++++++++- src/client/web/src/components/panes.tsx | 10 +- .../components/{panel.tsx => root_frame.tsx} | 27 +++--- src/client/web/src/components/state_mgr.tsx | 30 ++---- src/client/web/src/test/helpers.ts | 3 + .../web/src/worker/upload.baseworker.ts | 10 +- src/client/web/src/worker/uploader.ts | 11 +++ src/fs/local/fs.go | 16 ++-- 12 files changed, 301 insertions(+), 70 deletions(-) create mode 100644 src/client/web/src/components/__test__/panes.test.tsx create mode 100644 src/client/web/src/components/__test__/root_frame.test.tsx rename src/client/web/src/components/{panel.tsx => root_frame.tsx} (80%) diff --git a/src/client/web/src/components/__test__/browser.test.tsx b/src/client/web/src/components/__test__/browser.test.tsx index 2ba5260..a2f8441 100644 --- a/src/client/web/src/components/__test__/browser.test.tsx +++ b/src/client/web/src/components/__test__/browser.test.tsx @@ -1,13 +1,20 @@ import { List, Map } from "immutable"; -import { mock, instance } from "ts-mockito"; +import { mock, instance, anyString, when } from "ts-mockito"; -import { initWithWorker } from "../core_state"; -import { makePromise, makeNumberResponse } from "../../test/helpers"; -import { Updater } from "../browser"; +import { ICoreState, initWithWorker, mockState } from "../core_state"; +import { + makePromise, + makeNumberResponse, + mockUpdate, +} from "../../test/helpers"; +import { Updater, Browser } from "../browser"; import { MockUsersClient } from "../../client/users_mock"; -import { FilesClient } from "../../client/files_mock"; -import { MetadataResp } from "../../client"; -import { MockWorker } from "../../worker/interface"; +import { UsersClient } from "../../client/users"; +import { FilesClient } from "../../client/files"; +import { FilesClient as MockFilesClient } from "../../client/files_mock"; +import { MetadataResp, UploadInfo } from "../../client"; +import { MockWorker, UploadEntry } from "../../worker/interface"; +import { UploadMgr } from "../../worker/upload_mgr"; describe("Browser", () => { const mockWorkerClass = mock(MockWorker); @@ -41,7 +48,7 @@ describe("Browser", () => { ]; const usersClient = new MockUsersClient(""); - const filesClient = new FilesClient(""); + const filesClient = new MockFilesClient(""); for (let i = 0; i < tests.length; i++) { const tc = tests[i]; @@ -102,7 +109,7 @@ describe("Browser", () => { ]; const usersClient = new MockUsersClient(""); - const filesClient = new FilesClient(""); + const filesClient = new MockFilesClient(""); for (let i = 0; i < tests.length; i++) { const tc = tests[i]; @@ -163,7 +170,7 @@ describe("Browser", () => { ]; const usersClient = new MockUsersClient(""); - const filesClient = new FilesClient(""); + const filesClient = new MockFilesClient(""); for (let i = 0; i < tests.length; i++) { const tc = tests[i]; @@ -190,4 +197,72 @@ describe("Browser", () => { }); } }); + + xtest("Browser: deleteUploading", async () => { + interface TestCase { + deleteFile: string; + preState: ICoreState; + postState: ICoreState; + } + + const tcs: any = [ + { + deleteFile: "./path/file", + preState: { + browser: { + uploadings: List([ + { + realFilePath: "./path/file", + size: 1, + uploaded: 0, + }, + ]), + update: mockUpdate, + }, + }, + postState: { + browser: { + uploadings: List(), + update: mockUpdate, + }, + }, + }, + ]; + + const setState = (patch: any, state: ICoreState): ICoreState => { + state.panel.browser = patch.browser; + return state; + }; + const mockFilesClientClass = mock(FilesClient); + when(mockFilesClientClass.deleteUploading(anyString())).thenResolve({ + status: 200, + statusText: "", + data: "", + }); + // TODO: the return should dpends on test case + when(mockFilesClientClass.listUploadings()).thenResolve({ + status: 200, + statusText: "", + data: { uploadInfos: Array() }, + }); + + const mockUsersClientClass = mock(UsersClient); + + const mockFilesClient = instance(mockFilesClientClass); + const mockUsersClient = instance(mockUsersClientClass); + tcs.forEach((tc: TestCase) => { + const preState = setState(tc.preState, mockState()); + const postState = setState(tc.postState, mockState()); + // const existingFileName = preState.panel.browser.uploadings.get(0).realFilePath; + const infos:Map = Map(); + UploadMgr._setInfos(infos); + + const component = new Browser(preState.panel.browser); + Updater.init(preState.panel.browser); + Updater.setClients(mockUsersClient, mockFilesClient); + + component.deleteUploading(tc.deleteFile); + expect(Updater.props).toEqual(postState.panel.browser); + }); + }); }); diff --git a/src/client/web/src/components/__test__/panes.test.tsx b/src/client/web/src/components/__test__/panes.test.tsx new file mode 100644 index 0000000..d5726e9 --- /dev/null +++ b/src/client/web/src/components/__test__/panes.test.tsx @@ -0,0 +1,49 @@ +import { Set } from "immutable"; + +import { ICoreState, mockState } from "../core_state"; +import { Panes, Updater } from "../panes"; +import { mockUpdate } from "../../test/helpers"; + +describe("Panes", () => { + test("Panes: closePane", async () => { + interface TestCase { + preState: ICoreState; + postState: ICoreState; + } + + const tcs: any = [ + { + preState: { + panes: { + displaying: "settings", + paneNames: Set(["settings", "login"]), + update: mockUpdate, + }, + }, + postState: { + panes: { + displaying: "", + paneNames: Set(["settings", "login"]), + update: mockUpdate, + }, + }, + }, + ]; + + const setState = (patch: any, state: ICoreState): ICoreState => { + state.panel.panes = patch.panes; + return state; + }; + + tcs.forEach((tc: TestCase) => { + const preState = setState(tc.preState, mockState()); + const postState = setState(tc.postState, mockState()); + + const component = new Panes(preState.panel.panes); + Updater.init(preState.panel.panes); + + component.closePane(); + expect(Updater.props).toEqual(postState.panel.panes); + }); + }); +}); diff --git a/src/client/web/src/components/__test__/root_frame.test.tsx b/src/client/web/src/components/__test__/root_frame.test.tsx new file mode 100644 index 0000000..c81160c --- /dev/null +++ b/src/client/web/src/components/__test__/root_frame.test.tsx @@ -0,0 +1,52 @@ +import { Set } from "immutable"; +// import { mock, instance } from "ts-mockito"; + +import { ICoreState, mockState } from "../core_state"; +import { RootFrame } from "../root_frame"; +import { Updater } from "../panes"; + +describe("RootFrame", () => { + test("component: showSettings", async () => { + interface TestCase { + preState: ICoreState; + postState: ICoreState; + } + + const mockUpdate = (apply: (prevState: ICoreState) => ICoreState): void => {}; + const tcs: any = [ + { + preState: { + displaying: "", + panes: { + displaying: "", + paneNames: Set(["settings", "login"]), + }, + update: mockUpdate, + }, + postState: { + displaying: "settings", + panes: { + displaying: "settings", + paneNames: Set(["settings", "login"]), + }, + update: mockUpdate, + }, + }, + ]; + + const setState = (patch: any, state: ICoreState): ICoreState => { + return { ...state, panel: { ...state.panel, ...patch } }; + }; + + tcs.forEach((tc: TestCase) => { + const preState = setState(tc.preState, mockState()); + const postState = setState(tc.postState, mockState()); + + const component = new RootFrame(preState.panel); + Updater.init(preState.panel.panes); + + component.showSettings(); + expect(Updater.props).toEqual(postState.panel.panes); + }); + }); +}); diff --git a/src/client/web/src/components/browser.tsx b/src/client/web/src/components/browser.tsx index 94a441d..add79dc 100644 --- a/src/client/web/src/components/browser.tsx +++ b/src/client/web/src/components/browser.tsx @@ -46,7 +46,7 @@ function getItemPath(dirPath: string, itemName: string): string { } export class Updater { - private static props: Props; + static props: Props; private static usersClient: IUsersClient; private static filesClient: IFilesClient; @@ -160,8 +160,12 @@ export class Updater { static addUploadFiles = (fileList: FileList, len: number) => { for (let i = 0; i < len; i++) { + const filePath = getItemPath( + Updater.props.dirPath.join("/"), + fileList[i].name + ); // do not wait for the promise - UploadMgr.add(fileList[i], fileList[i].name); + UploadMgr.add(fileList[i], filePath); } Updater.setUploadings(UploadMgr.list()); }; @@ -215,9 +219,6 @@ export class Browser extends React.Component { }); } - // showPane = () => { - // this.setState({ show: !this.state.show }); - // }; onInputChange = (ev: React.ChangeEvent) => { this.setState({ inputValue: ev.target.value }); }; @@ -245,7 +246,15 @@ export class Browser extends React.Component { }; onMkDir = () => { - Updater.mkDir(this.state.inputValue) + if (this.state.inputValue === "") { + alert("folder name can not be empty"); + } + + const dirPath = getItemPath( + this.props.dirPath.join("/"), + this.state.inputValue + ); + Updater.mkDir(dirPath) .then(() => { this.setState({ inputValue: "" }); return Updater.setItems(this.props.dirPath); @@ -433,7 +442,7 @@ export class Browser extends React.Component { @@ -467,7 +476,7 @@ export class Browser extends React.Component { type="button" onClick={() => this.select(item.name)} className={`white-font ${isSelected ? "blue0-bg" : ""}`} - style={{width: "8rem", display: "inline-block"}} + style={{ width: "8rem", display: "inline-block" }} > {isSelected ? "Deselect" : "Select"} diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index 58a65fc..bc368ce 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -3,11 +3,19 @@ import { List, Set } from "immutable"; import BgWorker from "../worker/upload.bg.worker"; import { FgWorker } from "../worker/upload.fgworker"; -import { Props as PanelProps } from "./panel"; +import { Props as PanelProps } from "./root_frame"; import { Item } from "./browser"; import { UploadInfo } from "../client"; import { UploadMgr, IWorker } from "../worker/upload_mgr"; +export class BaseUpdater { + public static props: any; + public static init = (props: any) => (BaseUpdater.props = { ...props }); + public static apply = (prevState: ICoreState): ICoreState => { + throw Error("apply is not implemented"); + }; +} + export interface IContext { update: (targetStatePatch: any) => void; } @@ -25,11 +33,8 @@ export function initWithWorker(worker: IWorker): ICoreState { export function init(): ICoreState { const scripts = Array.from(document.querySelectorAll("script")); - if (!Worker) { - alert("web worker is not supported"); - } + const worker = Worker == null ? new FgWorker() : new BgWorker(); - const worker = new BgWorker(); UploadMgr.init(worker); return initState(); } @@ -65,3 +70,31 @@ export function initState(): ICoreState { }, }; } + +export function mockState(): ICoreState { + return { + ctx: undefined, + isVertical: false, + panel: { + displaying: "browser", + authPane: { + authed: false, + }, + browser: { + isVertical: false, + dirPath: List(["."]), + items: List([]), + uploadings: List([]), + uploadValue: "", + uploadFiles: List([]), + }, + panes: { + displaying: "", + paneNames: Set(["settings", "login"]), + login: { + authed: false, + }, + }, + }, + }; +} diff --git a/src/client/web/src/components/panes.tsx b/src/client/web/src/components/panes.tsx index 152a11c..b24eec1 100644 --- a/src/client/web/src/components/panes.tsx +++ b/src/client/web/src/components/panes.tsx @@ -13,7 +13,7 @@ export interface Props { } export class Updater { - private static props: Props; + static props: Props; static init = (props: Props) => (Updater.props = { ...props }); @@ -44,17 +44,15 @@ export class Updater { export interface State {} export class Panes extends React.Component { - private update: (updater: (prevState: ICoreState) => ICoreState) => void; constructor(p: Props) { super(p); Updater.init(p); - this.update = p.update; } closePane = () => { if (this.props.displaying !== "login") { Updater.displayPane(""); - this.update(Updater.updateState); + this.props.update(Updater.updateState); } }; @@ -66,8 +64,8 @@ export class Panes extends React.Component { } const panesMap: Map = Map({ - settings: , - login: , + settings: , + login: , }); const panes = panesMap.keySeq().map( diff --git a/src/client/web/src/components/panel.tsx b/src/client/web/src/components/root_frame.tsx similarity index 80% rename from src/client/web/src/components/panel.tsx rename to src/client/web/src/components/root_frame.tsx index b08ede0..e90c542 100644 --- a/src/client/web/src/components/panel.tsx +++ b/src/client/web/src/components/root_frame.tsx @@ -1,24 +1,22 @@ import * as React from "react"; -import { ICoreState } from "./core_state"; +import { ICoreState, BaseUpdater } from "./core_state"; import { Browser, Props as BrowserProps } from "./browser"; -import { AuthPane, Props as AuthPaneProps } from "./pane_login"; +import { Props as PaneLoginProps } from "./pane_login"; import { Panes, Props as PanesProps, Updater as PanesUpdater } from "./panes"; export interface Props { displaying: string; browser: BrowserProps; - authPane: AuthPaneProps; + authPane: PaneLoginProps; panes: PanesProps; update?: (updater: (prevState: ICoreState) => ICoreState) => void; } export class Updater { - private static props: Props; - - static init = (props: Props) => (Updater.props = { ...props }); - - static setPanel = (prevState: ICoreState): ICoreState => { + public static props: Props; + public static init = (props: Props) => (BaseUpdater.props = { ...props }); + public static apply = (prevState: ICoreState): ICoreState => { return { ...prevState, panel: { ...prevState.panel, ...Updater.props }, @@ -27,20 +25,19 @@ export class Updater { } export interface State {} -export class Panel extends React.Component { - private update: (updater: (prevState: ICoreState) => ICoreState) => void; +export class RootFrame extends React.Component { constructor(p: Props) { super(p); Updater.init(p); - this.update = p.update; } showSettings = () => { PanesUpdater.displayPane("settings"); - this.update(PanesUpdater.updateState); + this.props.update(PanesUpdater.updateState); }; render() { + const update = this.props.update; return (
@@ -48,7 +45,7 @@ export class Panel extends React.Component { displaying={this.props.panes.displaying} paneNames={this.props.panes.paneNames} login={this.props.authPane} - update={this.update} + update={update} />
{ dirPath={this.props.browser.dirPath} items={this.props.browser.items} uploadings={this.props.browser.uploadings} - update={this.update} + update={update} uploadFiles={this.props.browser.uploadFiles} uploadValue={this.props.browser.uploadValue} isVertical={this.props.browser.isVertical} />
+
Quickshare - sharing in simple way.
+
); diff --git a/src/client/web/src/components/state_mgr.tsx b/src/client/web/src/components/state_mgr.tsx index d5646af..e492808 100644 --- a/src/client/web/src/components/state_mgr.tsx +++ b/src/client/web/src/components/state_mgr.tsx @@ -1,40 +1,30 @@ import * as React from "react"; import { ICoreState, init } from "./core_state"; -import { Panel } from "./panel"; +import { RootFrame } from "./root_frame"; export interface Props {} export interface State extends ICoreState {} -export class UpdaterBase { - private static props: any; - static init = (props: any) => (UpdaterBase.props = {...props}); -} - export class StateMgr extends React.Component { constructor(p: Props) { super(p); this.state = init(); } - // TODO: any can be eliminated by adding union type of children states - update = (updater: (prevState:ICoreState) => ICoreState): void => { - console.log("before", this.state) - this.setState(updater(this.state)); - console.log("after", this.state) + update = (apply: (prevState: ICoreState) => ICoreState): void => { + this.setState(apply(this.state)); }; render() { return ( -
- -
+ ); } } diff --git a/src/client/web/src/test/helpers.ts b/src/client/web/src/test/helpers.ts index 555eb97..3f0ad21 100644 --- a/src/client/web/src/test/helpers.ts +++ b/src/client/web/src/test/helpers.ts @@ -1,4 +1,5 @@ import { Response } from "../client"; +import { ICoreState } from "../components/core_state"; export const makePromise = (ret: any): Promise => { return new Promise((resolve) => { @@ -13,3 +14,5 @@ export const makeNumberResponse = (status: number): Promise => { data: {}, }); }; + +export const mockUpdate = (apply: (prevState: ICoreState) => ICoreState): void => {}; diff --git a/src/client/web/src/worker/upload.baseworker.ts b/src/client/web/src/worker/upload.baseworker.ts index f9eb6a2..43460ed 100644 --- a/src/client/web/src/worker/upload.baseworker.ts +++ b/src/client/web/src/worker/upload.baseworker.ts @@ -14,7 +14,7 @@ export class UploadWorker { private file: File = undefined; private filePath: string = undefined; private uploader: FileUploader = undefined; - sendEvent = (resp: FileWorkerResp):void => { + sendEvent = (resp: FileWorkerResp): void => { // TODO: make this abstract throw new Error("not implemented"); }; @@ -30,6 +30,8 @@ export class UploadWorker { stopUploader = () => { if (this.uploader != null) { this.uploader.stop(); + this.file = undefined; + this.filePath = undefined; } }; getFilePath = (): string => { @@ -49,6 +51,7 @@ export class UploadWorker { // find the first qualified task const syncReq = req as SyncReq; const infoArray = syncReq.infos; + for (let i = 0; i < infoArray.length; i++) { if ( infoArray[i].runnable && @@ -59,6 +62,11 @@ export class UploadWorker { this.startUploader(infoArray[i].file, infoArray[i].filePath); } break; + } else if ( + !infoArray[i].runnable && + infoArray[i].filePath == this.filePath + ) { + this.stopUploader(); } } break; diff --git a/src/client/web/src/worker/uploader.ts b/src/client/web/src/worker/uploader.ts index a96380f..e113703 100644 --- a/src/client/web/src/worker/uploader.ts +++ b/src/client/web/src/worker/uploader.ts @@ -8,6 +8,7 @@ const speedDownRatio = 0.5; const speedUpRatio = 1.05; const createRetryLimit = 2; const uploadRetryLimit = 1024; +const backoffMax = 2000; export interface IFileUploader { stop: () => void; @@ -71,6 +72,13 @@ export class FileUploader { this.client = client; }; + backOff = async (): Promise => { + return new Promise((resolve) => { + const delay = Math.floor(Math.random() * backoffMax); + setTimeout(resolve, delay); + }); + }; + start = async (): Promise => { let resp: Response; @@ -81,6 +89,7 @@ export class FileUploader { return await this.upload(); } } catch (e) { + await this.backOff(); console.error(e); } } @@ -153,6 +162,8 @@ export class FileUploader { ); break; } + + await this.backOff(); } } catch (e) { this.errMsgs.push(e.toString()); diff --git a/src/fs/local/fs.go b/src/fs/local/fs.go index 2e40f14..9e0fc26 100644 --- a/src/fs/local/fs.go +++ b/src/fs/local/fs.go @@ -99,6 +99,16 @@ func (fs *LocalFS) translate(name string) (string, error) { } func (fs *LocalFS) Create(path string) error { + fs.opensMtx.Lock() + defer fs.opensMtx.Unlock() + if len(fs.opens) > fs.opensLimit { + err := fs.closeOpens(true, map[string]bool{}) + if err != nil { + return fmt.Errorf("too many opens and fail to clean: %w", err) + } + return ErrTooManyOpens + } + fullpath, err := fs.translate(path) if err != nil { return err @@ -109,12 +119,6 @@ func (fs *LocalFS) Create(path string) error { return err } - fs.opensMtx.Lock() - defer fs.opensMtx.Unlock() - if len(fs.opens) > fs.opensLimit { - return ErrTooManyOpens - } - fs.opens[fullpath] = &fileInfo{ lastAccess: time.Now(), fd: fd,