From 9267dc9d6a7c0f4494d2b49ffa5ab6f553352927 Mon Sep 17 00:00:00 2001 From: fatme Date: Mon, 18 Jun 2018 15:51:58 +0300 Subject: [PATCH 1/2] Speed up android livesync --- lib/common | 2 +- lib/constants.ts | 1 + lib/definitions/files-hash-service.d.ts | 22 +++++++++++ lib/services/android-project-service.ts | 5 ++- lib/services/files-hash-service.ts | 25 ++++++++++++ ...android-device-livesync-sockets-service.ts | 27 +++++++------ lib/services/platform-service.ts | 39 ++++++++++++++++++- 7 files changed, 107 insertions(+), 14 deletions(-) diff --git a/lib/common b/lib/common index 3e9e870c06..c78d5bdf7b 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 3e9e870c06461285d1a3f3f6b8163a972208d4b0 +Subproject commit c78d5bdf7ba550fe19812e39f08e3690834149d5 diff --git a/lib/constants.ts b/lib/constants.ts index b1858470a0..aa0e764f84 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -39,6 +39,7 @@ export const CONFIG_NS_APP_RESOURCES_ENTRY = "appResourcesPath"; export const CONFIG_NS_APP_ENTRY = "appPath"; export const DEPENDENCIES_JSON_NAME = "dependencies.json"; export const APK_EXTENSION_NAME = ".apk"; +export const HASHES_FILE_NAME = ".nshashes"; export class PackageVersion { static NEXT = "next"; diff --git a/lib/definitions/files-hash-service.d.ts b/lib/definitions/files-hash-service.d.ts index 753d63d65a..da5d4d6e3e 100644 --- a/lib/definitions/files-hash-service.d.ts +++ b/lib/definitions/files-hash-service.d.ts @@ -1,5 +1,27 @@ interface IFilesHashService { generateHashes(files: string[]): Promise; + /** + * Generate hashes for all prepared files (all files from app folder under platforms folder). + * @param platformData - Current platform's data + * @returns {Promise} + * A map with key file's path and value - file's hash + */ + generateHashesForProject(platformData: IPlatformData): Promise; + /** + * @param hashesFileDirectory - Path to directory containing the hash file. + * @returns {IStringDictionary} + * In case .nshashes file exists (under `hashesFileDirectory` directory), returns its content + * In case .nshashes file does not exist (under `hashesFileDirectory` directory), returns {} + */ + getGeneratedHashes(hashesFileDirectory: string): IStringDictionary; + /** + * Generates hashes for all prepared files (all files from app folder under platforms folder) + * and saves them in .nshashes file under `hashFileDirectory` directory. + * @param platformData - Current platform's data + * @param hashesFileDirectory - Path to directory containing the hash file. + * @returns {Promise} + */ + saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise; getChanges(files: string[], oldHashes: IStringDictionary): Promise; hasChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean; } diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index a336e71113..ba0fb98881 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -30,7 +30,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private $npm: INodePackageManager, private $androidPluginBuildService: IAndroidPluginBuildService, private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, - private $androidResourcesMigrationService: IAndroidResourcesMigrationService) { + private $androidResourcesMigrationService: IAndroidResourcesMigrationService, + private $filesHashService: IFilesHashService) { super($fs, $projectDataService); this.isAndroidStudioTemplate = false; } @@ -340,6 +341,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject message: "Gradle build..." }) ); + + await this.$filesHashService.saveHashesForProject(this._platformData, this._platformData.deviceBuildOutputPath); } private getGradleBuildOptions(settings: IAndroidBuildOptionsSettings, projectData: IProjectData): Array { diff --git a/lib/services/files-hash-service.ts b/lib/services/files-hash-service.ts index 45f1603843..1ad7813d02 100644 --- a/lib/services/files-hash-service.ts +++ b/lib/services/files-hash-service.ts @@ -1,5 +1,7 @@ import { executeActionByChunks } from "../common/helpers"; import { DEFAULT_CHUNK_SIZE } from "../common/constants"; +import { APP_FOLDER_NAME, HASHES_FILE_NAME } from "../constants"; +import * as path from "path"; export class FilesHashService implements IFilesHashService { constructor(private $fs: IFileSystem, @@ -24,6 +26,29 @@ export class FilesHashService implements IFilesHashService { return result; } + public async generateHashesForProject(platformData: IPlatformData): Promise { + const appFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const files = this.$fs.enumerateFilesInDirectorySync(appFilesPath); + const hashes = await this.generateHashes(files); + return hashes; + } + + public async saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise { + const hashes = await this.generateHashesForProject(platformData); + const hashesFilePath = path.join(hashesFileDirectory, HASHES_FILE_NAME); + this.$fs.writeJson(hashesFilePath, hashes); + } + + public getGeneratedHashes(hashesFileDirectory: string): IStringDictionary { + let result = {}; + const hashesFilePath = path.join(hashesFileDirectory, HASHES_FILE_NAME); + if (this.$fs.exists(hashesFilePath)) { + result = this.$fs.readJson(hashesFilePath); + } + + return result; + } + public async getChanges(files: string[], oldHashes: IStringDictionary): Promise { const newHashes = await this.generateHashes(files); return this.getChangesInShasums(oldHashes, newHashes); diff --git a/lib/services/livesync/android-device-livesync-sockets-service.ts b/lib/services/livesync/android-device-livesync-sockets-service.ts index aa9d27494b..c44de6d190 100644 --- a/lib/services/livesync/android-device-livesync-sockets-service.ts +++ b/lib/services/livesync/android-device-livesync-sockets-service.ts @@ -117,6 +117,11 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa return transferredFiles; } + public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { + const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); + return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); + } + private async _transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { await this.livesyncTool.sendFiles(localToDevicePaths.map(localToDevicePathData => localToDevicePathData.getLocalPath())); @@ -132,19 +137,24 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa private async _transferDirectory(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { let transferredLocalToDevicePaths: Mobile.ILocalToDevicePathData[]; const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier); - const currentShasums: IStringDictionary = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); - const oldShasums = await deviceHashService.getShasumsFromDevice(); + const currentHashes = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); + const oldHashes = await deviceHashService.getShasumsFromDevice(); + console.log("!!!!! OLD HASHES!!!!!!"); + console.log(oldHashes); - if (this.$options.force || !oldShasums) { + if (this.$options.force || !oldHashes) { + console.log("!!!!!!!!! NO OLD HASHES!!!!! THIS SHOULD NOT HAPPEN!!!!!!!"); await this.livesyncTool.sendDirectory(projectFilesPath); - await deviceHashService.uploadHashFileToDevice(currentShasums); + await deviceHashService.uploadHashFileToDevice(currentHashes); transferredLocalToDevicePaths = localToDevicePaths; } else { - const changedShasums = deviceHashService.getChangedShasums(oldShasums, currentShasums); + const changedShasums = deviceHashService.getChangedShasums(oldHashes, currentHashes); + console.log("CHANGEDSHASUMS!!!!!!!!!!!!!!!"); + console.log(changedShasums); const changedFiles = _.keys(changedShasums); if (changedFiles.length) { await this.livesyncTool.sendFiles(changedFiles); - await deviceHashService.uploadHashFileToDevice(currentShasums); + await deviceHashService.uploadHashFileToDevice(currentHashes); transferredLocalToDevicePaths = localToDevicePaths.filter(localToDevicePathData => changedFiles.indexOf(localToDevicePathData.getLocalPath()) >= 0); } else { transferredLocalToDevicePaths = []; @@ -165,9 +175,4 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa }); } } - - public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { - const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); - } } diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 984e5ed98b..3c3c40f307 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -41,7 +41,8 @@ export class PlatformService extends EventEmitter implements IPlatformService { private $projectChangesService: IProjectChangesService, private $analyticsService: IAnalyticsService, private $terminalSpinnerService: ITerminalSpinnerService, - private $pacoteService: IPacoteService + private $pacoteService: IPacoteService, + private $filesHashService: IFilesHashService ) { super(); } @@ -488,8 +489,18 @@ export class PlatformService extends EventEmitter implements IPlatformService { await platformData.platformProjectService.cleanDeviceTempFolder(device.deviceInfo.identifier, projectData); + const isApplicationInstalled = await this.isApplicationInstalled(device, projectData); + await device.applicationManager.reinstallApplication(projectData.projectId, packageFile); + await this.updateHashesOnDevice({ + device, + isApplicationInstalled, + appIdentifier: projectData.projectId, + outputFilePath, + platformData + }); + if (!buildConfig.release) { const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); const options = buildConfig; @@ -503,6 +514,32 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out(`Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`); } + private async updateHashesOnDevice(data: { device: Mobile.IDevice, isApplicationInstalled: boolean, appIdentifier: string, outputFilePath: string, platformData: IPlatformData }): Promise { + const { device, isApplicationInstalled, appIdentifier, platformData, outputFilePath } = data; + + if (!this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName)) { + return; + } + + let hashes = null; + if (isApplicationInstalled) { + hashes = await this.$filesHashService.generateHashesForProject(platformData); + } else { + hashes = this.$filesHashService.getGeneratedHashes(outputFilePath || platformData.deviceBuildOutputPath); + } + + await device.fileSystem.updateHashesOnDevice(hashes, appIdentifier); + } + + private async isApplicationInstalled(device: Mobile.IDevice, projectData: IProjectData): Promise { + let result = false; + if (this.$mobileHelper.isAndroidPlatform) { + result = await device.applicationManager.isApplicationInstalled(projectData.projectId); + } + + return result; + } + public async deployPlatform(deployInfo: IDeployPlatformInfo): Promise { await this.preparePlatform({ platform: deployInfo.platform, From 54003ccda582d494ceeffe39a15ae655526712a8 Mon Sep 17 00:00:00 2001 From: fatme Date: Fri, 10 Aug 2018 14:28:49 +0300 Subject: [PATCH 2/2] Refactor code in order to remove duplications of logic for generating hashes -> Extract all this logic to base class and reuse it from livesync with sockets and old livesync service Remove all $options from services --- lib/definitions/files-hash-service.d.ts | 10 +- lib/definitions/livesync.d.ts | 18 +- lib/helpers/livesync-command-helper.ts | 3 +- lib/services/files-hash-service.ts | 21 +- .../android-device-livesync-service-base.ts | 85 ++++ .../android-device-livesync-service.ts | 32 +- ...android-device-livesync-sockets-service.ts | 81 +--- .../livesync/device-livesync-service-base.ts | 4 +- lib/services/livesync/ios-livesync-service.ts | 2 +- lib/services/livesync/livesync-service.ts | 11 +- .../platform-livesync-service-base.ts | 8 +- lib/services/platform-service.ts | 28 +- test/debug.ts | 1 + .../android-device-livesync-service-base.ts | 381 ++++++++++++++++++ 14 files changed, 549 insertions(+), 136 deletions(-) create mode 100644 lib/services/livesync/android-device-livesync-service-base.ts create mode 100644 test/services/livesync/android-device-livesync-service-base.ts diff --git a/lib/definitions/files-hash-service.d.ts b/lib/definitions/files-hash-service.d.ts index da5d4d6e3e..e561119baa 100644 --- a/lib/definitions/files-hash-service.d.ts +++ b/lib/definitions/files-hash-service.d.ts @@ -7,13 +7,6 @@ interface IFilesHashService { * A map with key file's path and value - file's hash */ generateHashesForProject(platformData: IPlatformData): Promise; - /** - * @param hashesFileDirectory - Path to directory containing the hash file. - * @returns {IStringDictionary} - * In case .nshashes file exists (under `hashesFileDirectory` directory), returns its content - * In case .nshashes file does not exist (under `hashesFileDirectory` directory), returns {} - */ - getGeneratedHashes(hashesFileDirectory: string): IStringDictionary; /** * Generates hashes for all prepared files (all files from app folder under platforms folder) * and saves them in .nshashes file under `hashFileDirectory` directory. @@ -21,7 +14,8 @@ interface IFilesHashService { * @param hashesFileDirectory - Path to directory containing the hash file. * @returns {Promise} */ - saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise; + saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise; + saveHashes(hashes: IStringDictionary, hashesFileDirectory: string): void; getChanges(files: string[], oldHashes: IStringDictionary): Promise; hasChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean; } diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts index a2807ee1bf..b160c6034b 100644 --- a/lib/definitions/livesync.d.ts +++ b/lib/definitions/livesync.d.ts @@ -154,6 +154,13 @@ interface ILiveSyncInfo extends IProjectDir, IEnvOptions, IBundle, IRelease, IOp */ clean?: boolean; + /** + * Defines if initial sync will be forced. + * In case it is true, transfers all project's directory on device + * In case it is false, transfers only changed files. + */ + force?: boolean; + /** * Defines the timeout in seconds {N} CLI will wait to find the inspector socket port from device's logs. * If not provided, defaults to 10seconds. @@ -330,6 +337,8 @@ interface ILiveSyncWatchInfo extends IProjectDataComposition, IHasUseHotModuleRe filesToSync: string[]; isReinstalled: boolean; syncAllFiles: boolean; + liveSyncDeviceInfo: ILiveSyncDeviceInfo; + force?: boolean; } interface ILiveSyncResultInfo extends IHasUseHotModuleReloadOption { @@ -344,6 +353,13 @@ interface IFullSyncInfo extends IProjectDataComposition, IHasUseHotModuleReloadO device: Mobile.IDevice; watch: boolean; syncAllFiles: boolean; + liveSyncDeviceInfo: ILiveSyncDeviceInfo; + force?: boolean; +} + +interface ITransferFilesOptions { + isFullSync: boolean; + force?: boolean; } interface IPlatformLiveSyncService { @@ -383,7 +399,7 @@ interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase * @param {boolean} isFullSync Indicates if the operation is part of a fullSync * @return {Promise} Returns the ILocalToDevicePathData of all transfered files */ - transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise; + transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise; } interface IAndroidNativeScriptDeviceLiveSyncService extends INativeScriptDeviceLiveSyncService { diff --git a/lib/helpers/livesync-command-helper.ts b/lib/helpers/livesync-command-helper.ts index 25f3aa3e27..508ac83528 100644 --- a/lib/helpers/livesync-command-helper.ts +++ b/lib/helpers/livesync-command-helper.ts @@ -115,7 +115,8 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { release: this.$options.release, env: this.$options.env, timeout: this.$options.timeout, - useHotModuleReload: this.$options.hmr + useHotModuleReload: this.$options.hmr, + force: this.$options.force }; await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); diff --git a/lib/services/files-hash-service.ts b/lib/services/files-hash-service.ts index 1ad7813d02..f8893e4d99 100644 --- a/lib/services/files-hash-service.ts +++ b/lib/services/files-hash-service.ts @@ -33,20 +33,10 @@ export class FilesHashService implements IFilesHashService { return hashes; } - public async saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise { + public async saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise { const hashes = await this.generateHashesForProject(platformData); - const hashesFilePath = path.join(hashesFileDirectory, HASHES_FILE_NAME); - this.$fs.writeJson(hashesFilePath, hashes); - } - - public getGeneratedHashes(hashesFileDirectory: string): IStringDictionary { - let result = {}; - const hashesFilePath = path.join(hashesFileDirectory, HASHES_FILE_NAME); - if (this.$fs.exists(hashesFilePath)) { - result = this.$fs.readJson(hashesFilePath); - } - - return result; + this.saveHashes(hashes, hashesFileDirectory); + return hashes; } public async getChanges(files: string[], oldHashes: IStringDictionary): Promise { @@ -58,6 +48,11 @@ export class FilesHashService implements IFilesHashService { return !!_.keys(this.getChangesInShasums(oldHashes, newHashes)).length; } + public saveHashes(hashes: IStringDictionary, hashesFileDirectory: string): void { + const hashesFilePath = path.join(hashesFileDirectory, HASHES_FILE_NAME); + this.$fs.writeJson(hashesFilePath, hashes); + } + private getChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): IStringDictionary { return _.omitBy(newHashes, (hash: string, pathToFile: string) => !!_.find(oldHashes, (oldHash: string, oldPath: string) => pathToFile === oldPath && hash === oldHash)); } diff --git a/lib/services/livesync/android-device-livesync-service-base.ts b/lib/services/livesync/android-device-livesync-service-base.ts new file mode 100644 index 0000000000..eeddc7c32f --- /dev/null +++ b/lib/services/livesync/android-device-livesync-service-base.ts @@ -0,0 +1,85 @@ +import { DeviceLiveSyncServiceBase } from './device-livesync-service-base'; +import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; + +export abstract class AndroidDeviceLiveSyncServiceBase extends DeviceLiveSyncServiceBase { + private deviceHashServices: IDictionary; + + constructor(protected $injector: IInjector, + protected $platformsData: IPlatformsData, + protected $filesHashService: IFilesHashService, + protected $logger: ILogger, + protected device: Mobile.IAndroidDevice) { + super($platformsData, device); + this.deviceHashServices = {}; + } + + public abstract async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise; + public abstract async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise; + + public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { + const key = `${this.device.deviceInfo.identifier}${appIdentifier}`; + if (!this.deviceHashServices[key]) { + const deviceHashService = this.$injector.resolve(AndroidDeviceHashService, { adb: this.device.adb, appIdentifier }); + this.deviceHashServices[key] = deviceHashService; + } + + return this.deviceHashServices[key]; + } + + public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { + const transferredFiles = await this.transferFilesCore(deviceAppData, localToDevicePaths, projectFilesPath, options); + await this.updateHashes(deviceAppData, localToDevicePaths, projectData, liveSyncDeviceInfo); + return transferredFiles; + } + + private async transferFilesCore(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, options: ITransferFilesOptions): Promise { + if (options.force && options.isFullSync) { + const hashFileDevicePath = this.getDeviceHashService(deviceAppData.appIdentifier).hashFileDevicePath; + await this.device.fileSystem.deleteFile(hashFileDevicePath, deviceAppData.appIdentifier); + this.$logger.trace("Before transfer directory on device ", localToDevicePaths); + await this.transferDirectoryOnDevice(deviceAppData, localToDevicePaths, projectFilesPath); + return localToDevicePaths; + } + + const localToDevicePathsToTransfer = await this.getLocalToDevicePathsToTransfer(deviceAppData, localToDevicePaths, options); + this.$logger.trace("Files to transfer: ", localToDevicePathsToTransfer); + await this.transferFilesOnDevice(deviceAppData, localToDevicePathsToTransfer); + return localToDevicePathsToTransfer; + } + + private async getLocalToDevicePathsToTransfer(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], options: ITransferFilesOptions): Promise { + if (options.force || !options.isFullSync) { + return localToDevicePaths; + } + + const changedLocalToDevicePaths = await this.getChangedLocalToDevicePaths(deviceAppData.appIdentifier, localToDevicePaths); + return changedLocalToDevicePaths; + } + + private async getChangedLocalToDevicePaths(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + const deviceHashService = this.getDeviceHashService(appIdentifier); + const currentHashes = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); + const oldHashes = (await deviceHashService.getShasumsFromDevice()) || {}; + const changedHashes = deviceHashService.getChangedShasums(oldHashes, currentHashes); + const changedFiles = _.keys(changedHashes); + const changedLocalToDevicePaths = localToDevicePaths.filter(localToDevicePathData => changedFiles.indexOf(localToDevicePathData.getLocalPath()) >= 0); + return changedLocalToDevicePaths; + } + + private async updateHashes(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo): Promise { + const hashes = await this.updateHashesOnDevice(deviceAppData, localToDevicePaths, projectData, liveSyncDeviceInfo); + this.updateLocalHashes(hashes, deviceAppData, projectData, liveSyncDeviceInfo); + } + + private async updateHashesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo): Promise { + const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier); + const currentHashes = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); + await deviceHashService.uploadHashFileToDevice(currentHashes); + return currentHashes; + } + + private updateLocalHashes(hashes: IStringDictionary, deviceAppData: Mobile.IDeviceAppData, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo): void { + const hashFilePath = liveSyncDeviceInfo.outputPath || this.$platformsData.getPlatformData(deviceAppData.platform, projectData).deviceBuildOutputPath; + this.$filesHashService.saveHashes(hashes, hashFilePath); + } +} diff --git a/lib/services/livesync/android-device-livesync-service.ts b/lib/services/livesync/android-device-livesync-service.ts index c212124242..fc979eb591 100644 --- a/lib/services/livesync/android-device-livesync-service.ts +++ b/lib/services/livesync/android-device-livesync-service.ts @@ -1,23 +1,29 @@ -import { DeviceAndroidDebugBridge } from "../../common/mobile/android/device-android-debug-bridge"; -import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; -import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; +import { AndroidDeviceLiveSyncServiceBase } from "./android-device-livesync-service-base"; import * as helpers from "../../common/helpers"; import { LiveSyncPaths } from "../../common/constants"; -import { cache } from "../../common/decorators"; import * as path from "path"; import * as net from "net"; -export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { +export class AndroidDeviceLiveSyncService extends AndroidDeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { private port: number; - constructor( - private $mobileHelper: Mobile.IMobileHelper, + constructor(private $mobileHelper: Mobile.IMobileHelper, private $devicePathProvider: IDevicePathProvider, - private $injector: IInjector, + $injector: IInjector, private $androidProcessService: Mobile.IAndroidProcessService, protected $platformsData: IPlatformsData, - protected device: Mobile.IAndroidDevice) { - super($platformsData, device); + protected device: Mobile.IAndroidDevice, + $filesHashService: IFilesHashService, + $logger: ILogger) { + super($injector, $platformsData, $filesHashService, $logger, device); + } + + public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + await this.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); + } + + public async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { + await this.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); } public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { @@ -116,12 +122,6 @@ export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase impl await this.getDeviceHashService(deviceAppData.appIdentifier).removeHashes(localToDevicePaths); } - @cache() - public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { - const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); - } - private async awaitRuntimeReloadSuccessMessage(): Promise { return new Promise((resolve, reject) => { let isResolved = false; diff --git a/lib/services/livesync/android-device-livesync-sockets-service.ts b/lib/services/livesync/android-device-livesync-sockets-service.ts index c44de6d190..dec5569300 100644 --- a/lib/services/livesync/android-device-livesync-sockets-service.ts +++ b/lib/services/livesync/android-device-livesync-sockets-service.ts @@ -1,6 +1,4 @@ -import { DeviceAndroidDebugBridge } from "../../common/mobile/android/device-android-debug-bridge"; -import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; -import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; +import { AndroidDeviceLiveSyncServiceBase } from "./android-device-livesync-service-base"; import { APP_FOLDER_NAME } from "../../constants"; import { LiveSyncPaths } from "../../common/constants"; import { AndroidLivesyncTool } from "./android-livesync-tool"; @@ -8,24 +6,25 @@ import * as path from "path"; import * as temp from "temp"; import * as semver from "semver"; -export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { +export class AndroidDeviceSocketsLiveSyncService extends AndroidDeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { private livesyncTool: IAndroidLivesyncTool; private static STATUS_UPDATE_INTERVAL = 10000; private static MINIMAL_VERSION_LONG_LIVING_CONNECTION = "0.2.0"; constructor( private data: IProjectData, - private $injector: IInjector, + $injector: IInjector, protected $platformsData: IPlatformsData, protected $staticConfig: Config.IStaticConfig, - private $logger: ILogger, + $logger: ILogger, protected device: Mobile.IAndroidDevice, private $options: IOptions, private $processService: IProcessService, private $fs: IFileSystem, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { - super($platformsData, device); - this.livesyncTool = this.$injector.resolve(AndroidLivesyncTool); + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $filesHashService: IFilesHashService) { + super($injector, $platformsData, $filesHashService, $logger, device); + this.livesyncTool = this.$injector.resolve(AndroidLivesyncTool); } public async beforeLiveSyncAction(deviceAppData: Mobile.IDeviceAppData): Promise { @@ -101,67 +100,17 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { await this.livesyncTool.removeFiles(_.map(localToDevicePaths, (element: any) => element.filePath)); - - await this.getDeviceHashService(deviceAppData.appIdentifier).removeHashes(localToDevicePaths); - } - - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { - let transferredFiles; - - if (isFullSync) { - transferredFiles = await this._transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); - } else { - transferredFiles = await this._transferFiles(deviceAppData, localToDevicePaths); - } - - return transferredFiles; - } - - public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { - const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); - } - - private async _transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - await this.livesyncTool.sendFiles(localToDevicePaths.map(localToDevicePathData => localToDevicePathData.getLocalPath())); - - // Update hashes const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier); - if (! await deviceHashService.updateHashes(localToDevicePaths)) { - this.$logger.trace("Unable to find hash file on device. The next livesync command will create it."); - } - - return localToDevicePaths; + await deviceHashService.removeHashes(localToDevicePaths); } - private async _transferDirectory(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { - let transferredLocalToDevicePaths: Mobile.ILocalToDevicePathData[]; - const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier); - const currentHashes = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); - const oldHashes = await deviceHashService.getShasumsFromDevice(); - console.log("!!!!! OLD HASHES!!!!!!"); - console.log(oldHashes); - - if (this.$options.force || !oldHashes) { - console.log("!!!!!!!!! NO OLD HASHES!!!!! THIS SHOULD NOT HAPPEN!!!!!!!"); - await this.livesyncTool.sendDirectory(projectFilesPath); - await deviceHashService.uploadHashFileToDevice(currentHashes); - transferredLocalToDevicePaths = localToDevicePaths; - } else { - const changedShasums = deviceHashService.getChangedShasums(oldHashes, currentHashes); - console.log("CHANGEDSHASUMS!!!!!!!!!!!!!!!"); - console.log(changedShasums); - const changedFiles = _.keys(changedShasums); - if (changedFiles.length) { - await this.livesyncTool.sendFiles(changedFiles); - await deviceHashService.uploadHashFileToDevice(currentHashes); - transferredLocalToDevicePaths = localToDevicePaths.filter(localToDevicePathData => changedFiles.indexOf(localToDevicePathData.getLocalPath()) >= 0); - } else { - transferredLocalToDevicePaths = []; - } - } + public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + const files = _.map(localToDevicePaths, localToDevicePath => localToDevicePath.getLocalPath()); + await this.livesyncTool.sendFiles(files); + } - return transferredLocalToDevicePaths; + public async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { + await this.livesyncTool.sendDirectory(projectFilesPath); } private async connectLivesyncTool(appIdentifier: string) { diff --git a/lib/services/livesync/device-livesync-service-base.ts b/lib/services/livesync/device-livesync-service-base.ts index 9b46395a96..e9e848d8bd 100644 --- a/lib/services/livesync/device-livesync-service-base.ts +++ b/lib/services/livesync/device-livesync-service-base.ts @@ -27,10 +27,10 @@ export abstract class DeviceLiveSyncServiceBase { return fastSyncFileExtensions; } - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { + public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { let transferredFiles: Mobile.ILocalToDevicePathData[] = []; - if (isFullSync) { + if (options.isFullSync) { transferredFiles = await this.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); } else { transferredFiles = await this.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); diff --git a/lib/services/livesync/ios-livesync-service.ts b/lib/services/livesync/ios-livesync-service.ts index 5ad992bb96..e2f3f03208 100644 --- a/lib/services/livesync/ios-livesync-service.ts +++ b/lib/services/livesync/ios-livesync-service.ts @@ -60,7 +60,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I public liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise { if (liveSyncInfo.isReinstalled) { // In this case we should execute fullsync because iOS Runtime requires the full content of app dir to be extracted in the root of sync dir. - return this.fullSync({ projectData: liveSyncInfo.projectData, device, syncAllFiles: liveSyncInfo.syncAllFiles, watch: true }); + return this.fullSync({ projectData: liveSyncInfo.projectData, device, syncAllFiles: liveSyncInfo.syncAllFiles, liveSyncDeviceInfo: liveSyncInfo.liveSyncDeviceInfo, watch: true }); } else { return super.liveSyncWatchAction(device, liveSyncInfo); } diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 4b597136f8..a7328c8c9d 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -505,10 +505,13 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi await platformLiveSyncService.prepareForLiveSync(device, projectData, liveSyncData, deviceBuildInfoDescriptor.debugOptions); const liveSyncResultInfo = await platformLiveSyncService.fullSync({ - projectData, device, + projectData, + device, syncAllFiles: liveSyncData.watchAllFiles, useHotModuleReload: liveSyncData.useHotModuleReload, - watch: !liveSyncData.skipWatcher + watch: !liveSyncData.skipWatcher, + force: liveSyncData.force, + liveSyncDeviceInfo: deviceBuildInfoDescriptor }); await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); @@ -637,12 +640,14 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi const service = this.getLiveSyncService(device.deviceInfo.platform); const settings: ILiveSyncWatchInfo = { + liveSyncDeviceInfo: deviceBuildInfoDescriptor, projectData, filesToRemove: currentFilesToRemove, filesToSync: currentFilesToSync, isReinstalled: appInstalledOnDeviceResult.appInstalled, syncAllFiles: liveSyncData.watchAllFiles, - useHotModuleReload: liveSyncData.useHotModuleReload + useHotModuleReload: liveSyncData.useHotModuleReload, + force: liveSyncData.force }; const liveSyncResultInfo = await service.liveSyncWatchAction(device, settings); diff --git a/lib/services/livesync/platform-livesync-service-base.ts b/lib/services/livesync/platform-livesync-service-base.ts index f2c64952f0..3189ae813d 100644 --- a/lib/services/livesync/platform-livesync-service-base.ts +++ b/lib/services/livesync/platform-livesync-service-base.ts @@ -47,7 +47,7 @@ export abstract class PlatformLiveSyncServiceBase { const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, []); - const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, true, projectData); + const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, syncInfo.liveSyncDeviceInfo, { isFullSync: true, force: syncInfo.force }); return { modifiedFilesData, @@ -86,7 +86,7 @@ export abstract class PlatformLiveSyncServiceBase { const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, existingFiles, []); modifiedLocalToDevicePaths.push(...localToDevicePaths); - modifiedLocalToDevicePaths = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, false, projectData); + modifiedLocalToDevicePaths = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncInfo.liveSyncDeviceInfo, { isFullSync: false, force: liveSyncInfo.force}); } } @@ -113,11 +113,11 @@ export abstract class PlatformLiveSyncServiceBase { }; } - protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean, projectData: IProjectData): Promise { + protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { let transferredFiles: Mobile.ILocalToDevicePathData[] = []; const deviceLiveSyncService = this.getDeviceLiveSyncService(deviceAppData.device, projectData); - transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync); + transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncDeviceInfo, options); this.logFilesSyncInformation(transferredFiles, "Successfully transferred %s.", this.$logger.info); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 3c3c40f307..bde73c5458 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -41,8 +41,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { private $projectChangesService: IProjectChangesService, private $analyticsService: IAnalyticsService, private $terminalSpinnerService: ITerminalSpinnerService, - private $pacoteService: IPacoteService, - private $filesHashService: IFilesHashService + private $pacoteService: IPacoteService ) { super(); } @@ -489,13 +488,10 @@ export class PlatformService extends EventEmitter implements IPlatformService { await platformData.platformProjectService.cleanDeviceTempFolder(device.deviceInfo.identifier, projectData); - const isApplicationInstalled = await this.isApplicationInstalled(device, projectData); - await device.applicationManager.reinstallApplication(projectData.projectId, packageFile); await this.updateHashesOnDevice({ device, - isApplicationInstalled, appIdentifier: projectData.projectId, outputFilePath, platformData @@ -514,32 +510,22 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out(`Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`); } - private async updateHashesOnDevice(data: { device: Mobile.IDevice, isApplicationInstalled: boolean, appIdentifier: string, outputFilePath: string, platformData: IPlatformData }): Promise { - const { device, isApplicationInstalled, appIdentifier, platformData, outputFilePath } = data; + private async updateHashesOnDevice(data: { device: Mobile.IDevice, appIdentifier: string, outputFilePath: string, platformData: IPlatformData }): Promise { + const { device, appIdentifier, platformData, outputFilePath } = data; if (!this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName)) { return; } - let hashes = null; - if (isApplicationInstalled) { - hashes = await this.$filesHashService.generateHashesForProject(platformData); - } else { - hashes = this.$filesHashService.getGeneratedHashes(outputFilePath || platformData.deviceBuildOutputPath); + let hashes = {}; + const hashesFilePath = path.join(outputFilePath || platformData.deviceBuildOutputPath, constants.HASHES_FILE_NAME); + if (this.$fs.exists(hashesFilePath)) { + hashes = this.$fs.readJson(hashesFilePath); } await device.fileSystem.updateHashesOnDevice(hashes, appIdentifier); } - private async isApplicationInstalled(device: Mobile.IDevice, projectData: IProjectData): Promise { - let result = false; - if (this.$mobileHelper.isAndroidPlatform) { - result = await device.applicationManager.isApplicationInstalled(projectData.projectId); - } - - return result; - } - public async deployPlatform(deployInfo: IDeployPlatformInfo): Promise { await this.preparePlatform({ platform: deployInfo.platform, diff --git a/test/debug.ts b/test/debug.ts index 8e55b0c22a..a5d25638b9 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -78,6 +78,7 @@ function createTestInjector(): IInjector { testInjector.register("androidPluginBuildService", stubs.AndroidPluginBuildServiceStub); testInjector.register("platformEnvironmentRequirements", {}); testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub); + testInjector.register("filesHashService", {}); return testInjector; } diff --git a/test/services/livesync/android-device-livesync-service-base.ts b/test/services/livesync/android-device-livesync-service-base.ts new file mode 100644 index 0000000000..97c793656e --- /dev/null +++ b/test/services/livesync/android-device-livesync-service-base.ts @@ -0,0 +1,381 @@ +import { Yok } from "../../../lib/common/yok"; +import { AndroidDeviceLiveSyncServiceBase } from "../../../lib/services/livesync/android-device-livesync-service-base"; +import { LiveSyncPaths } from "../../../lib/common/constants"; +import { assert } from "chai"; +import * as path from "path"; + +interface ITestSetupInput { + existsHashesFile?: boolean; + addChangedFile?: boolean; + addUnchangedFile?: boolean; + changedFileLocalName?: string; + unchangedFileLocalName?: string; +} + +interface ITestSetupOutput { + deviceAppData: Mobile.IDeviceAppData; + localToDevicePaths: Mobile.ILocalToDevicePathData[]; + androidDeviceLiveSyncServiceBase: any; + projectRoot: string; + changedFileLocalPath: string; + unchangedFileLocalPath: string; +} + +const transferFilesOnDeviceParams: any[] = []; +let transferDirectoryOnDeviceParams: any[] = []; +let resolveParams: any[] = []; +const deleteFileParams: any[] = []; +const writeJsonParams: any[] = []; +const pushFileParams: any[] = []; + +class AndroidDeviceLiveSyncServiceBaseMock extends AndroidDeviceLiveSyncServiceBase { + constructor($injector: IInjector, + $platformsData: any, + $filesHashService: any, + $logger: ILogger, + device: Mobile.IAndroidDevice) { + super($injector, $platformsData, $filesHashService, $logger, device); + } + + public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + transferFilesOnDeviceParams.push({ deviceAppData, localToDevicePaths }); + } + + public async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { + transferDirectoryOnDeviceParams.push({ deviceAppData, localToDevicePaths, projectFilesPath }); + } +} + +class LocalToDevicePathDataMock { + constructor(private filePath: string) { } + + public getLocalPath(): string { + return this.filePath; + } + + public getDevicePath(): string { + return `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${path.basename(this.filePath)}`; + } +} + +function mockPlatformsData() { + return { + getPlatformData: () => { + return { + deviceBuildOutputPath: "testDeviceBuildOutputPath" + }; + } + }; +} + +function createTestInjector() { + const injector = new Yok(); + injector.register("fs", {}); + injector.register("mobileHelper", { + buildDevicePath: (filePath: string) => filePath + }); + return injector; +} + +const device: Mobile.IAndroidDevice = { + deviceInfo: mockDeviceInfo(), + adb: mockAdb(), + applicationManager: mockDeviceApplicationManager(), + fileSystem: mockDeviceFileSystem(), + isEmulator: true, + openDeviceLogStream: () => Promise.resolve(), + getApplicationInfo: () => Promise.resolve(null), + init: () => Promise.resolve() +}; + +function mockDeviceInfo(): Mobile.IDeviceInfo { + return { + identifier: "testIdentifier", + displayName: "testDisplayName", + model: "testModel", + version: "8.0.0", + vendor: "Google", + status: "", + errorHelp: null, + isTablet: true, + type: "Device", + platform: "Android" + }; +} + +function createDeviceAppData(androidVersion?: string): Mobile.IDeviceAppData { + return { + getDeviceProjectRootPath: async () => `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${LiveSyncPaths.SYNC_DIR_NAME}`, + appIdentifier, + device: { + deviceInfo: { + version: androidVersion || "8.1.2" + } + }, + isLiveSyncSupported: async () => true, + platform: "Android" + }; +} + +function mockAdb(): Mobile.IDeviceAndroidDebugBridge { + return { + pushFile: async (filePath: string) => { + pushFileParams.push({ filePath }); + }, + sendBroadcastToDevice: async (action: string, extras?: IStringDictionary) => 1, + executeCommand: () => Promise.resolve(), + executeShellCommand: () => Promise.resolve(), + removeFile: () => Promise.resolve(), + getPropertyValue: async () => "testProperty", + getDevicesSafe: async () => [], + getDevices: async () => [] + }; +} + +function mockDeviceApplicationManager(): Mobile.IDeviceApplicationManager { + return {}; +} + +function mockDeviceFileSystem(): Mobile.IDeviceFileSystem { + return { + deleteFile: async (deviceFilePath, appIdentifier) => { + deleteFileParams.push({ deviceFilePath, appIdentifier }); + } + }; +} + +function mockFsStats(options: { isDirectory: boolean, isFile: boolean }): (filePath: string) => { isDirectory: () => boolean, isFile: () => boolean } { + return (filePath: string) => ({ + isDirectory: (): boolean => options.isDirectory, + isFile: (): boolean => options.isFile + }); +} + +function mockFilesHashService() { + return { + saveHashes: () => ({}) + }; +} + +function mockLogger(): any { + return { + trace: () => ({}) + }; +} + +function createAndroidDeviceLiveSyncServiceBase() { + const injector = new Yok(); + injector.resolve = (service, args) => resolveParams.push({ service, args }); + const androidDeviceLiveSyncServiceBase = new AndroidDeviceLiveSyncServiceBaseMock(injector, mockPlatformsData(), mockFilesHashService(), mockLogger(), device); + return androidDeviceLiveSyncServiceBase; +} + +function setup(options?: ITestSetupInput): ITestSetupOutput { + options = options || {}; + const projectRoot = "~/TestApp/app"; + const changedFileName = "test.js"; + const unchangedFileName = "notChangedFile.js"; + const changedFileLocalPath = `${projectRoot}/${options.changedFileLocalName || changedFileName}`; + const unchangedFileLocalPath = `${projectRoot}/${options.unchangedFileLocalName || unchangedFileName}`; + const filesToShasums: IStringDictionary = {}; + if (options.addChangedFile) { + filesToShasums[changedFileLocalPath] = "1"; + } + if (options.addUnchangedFile) { + filesToShasums[unchangedFileLocalPath] = "2"; + } + + const injector = createTestInjector(); + const localToDevicePaths = _.keys(filesToShasums).map(file => injector.resolve(LocalToDevicePathDataMock, { filePath: file })); + const deviceAppData = createDeviceAppData(); + const androidDeviceLiveSyncServiceBase = new AndroidDeviceLiveSyncServiceBaseMock(injector, mockPlatformsData(), mockFilesHashService(), mockLogger(), device); + + const fs = injector.resolve("fs"); + fs.exists = () => options.existsHashesFile; + fs.getFsStats = mockFsStats({ isDirectory: false, isFile: true }); + fs.getFileShasum = async (filePath: string) => filesToShasums[filePath]; + fs.writeJson = (filename: string, data: any) => writeJsonParams.push({ filename, data }); + fs.readJson = (filePath: string) => { + const deviceHashesFileContent: IStringDictionary = {}; + deviceHashesFileContent[`${projectRoot}/${changedFileName}`] = "11"; + if (options.addUnchangedFile) { + deviceHashesFileContent[`${projectRoot}/${unchangedFileName}`] = "2"; + } + + return deviceHashesFileContent; + }; + + return { + localToDevicePaths, + deviceAppData, + androidDeviceLiveSyncServiceBase, + projectRoot, + changedFileLocalPath, + unchangedFileLocalPath + }; +} + +const appIdentifier = "testAppIdentifier"; + +async function transferFiles(testSetup: ITestSetupOutput, options: { force: boolean, isFullSync: boolean}): Promise { + const androidDeviceLiveSyncServiceBase = testSetup.androidDeviceLiveSyncServiceBase; + const transferredFiles = await androidDeviceLiveSyncServiceBase.transferFiles( + testSetup.deviceAppData, + testSetup.localToDevicePaths, + testSetup.projectRoot, + {}, + {}, + options + ); + return transferredFiles; +} + +describe("AndroidDeviceLiveSyncServiceBase", () => { + describe("getDeviceHashService", () => { + beforeEach(() => { + resolveParams = []; + }); + it("should resolve AndroidDeviceHashService when the key is not stored in dictionary", () => { + const androidDeviceLiveSyncServiceBase = createAndroidDeviceLiveSyncServiceBase(); + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + }); + it("should return already stored value when the method is called for second time with the same deviceIdentifier and appIdentifier", () => { + const androidDeviceLiveSyncServiceBase = createAndroidDeviceLiveSyncServiceBase(); + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + }); + it("should return AndroidDeviceHashService when the method is called for second time with different appIdentifier and same deviceIdentifier", () => { + const androidDeviceLiveSyncServiceBase = createAndroidDeviceLiveSyncServiceBase(); + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + + const newAppIdentifier = "myNewAppIdentifier"; + androidDeviceLiveSyncServiceBase.getDeviceHashService(newAppIdentifier); + assert.equal(resolveParams.length, 2); + assert.isFunction(resolveParams[1].service); + assert.isDefined(resolveParams[1].args.adb); + assert.equal(resolveParams[1].args.appIdentifier, newAppIdentifier); + }); + }); + + describe("transferFiles", () => { + beforeEach(() => { + transferDirectoryOnDeviceParams = []; + }); + + describe("when is full sync", () => { + it("transfers the whole directory when force option is specified", async () => { + const testSetup = setup({ + addChangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: true, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + assert.equal(transferDirectoryOnDeviceParams.length, 1); + assert.equal(transferDirectoryOnDeviceParams[0].localToDevicePaths.length, 1); + }); + it("transfers only changed files when there are file changes", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + it("transfers only changed files when there are both changed and not changed files", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + addUnchangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + it("does not transfer files when no file changes", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: false + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 0); + }); + it("transfers files which has different location and there are changed files", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + addUnchangedFile: true, + unchangedFileLocalName: "newLocation/notChangedFile.js" + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 2); + assert.deepEqual(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + assert.deepEqual(transferredFiles[1].getLocalPath(), testSetup.unchangedFileLocalPath); + }); + it("transfers files which has different location and no changed files", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: false, + addUnchangedFile: true, + unchangedFileLocalName: "newLocation/notChangedFile.js" + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.deepEqual(transferredFiles[0].getLocalPath(), testSetup.unchangedFileLocalPath); + }); + it("transfers changed files with different location", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + changedFileLocalName: "newLocation/test.js" + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + }); + + describe("when is not full sync", () => { + it("does not transfer the whole directory when force option is specified", async () => { + const testSetup = setup({ + addChangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: true, isFullSync: false }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + assert.equal(transferDirectoryOnDeviceParams.length, 0); + }); + it("transfers all provided files", async () => { + const testSetup = setup({ + addChangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: false }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + }); + }); +});