forked from microsoft/vscode-pull-request-github
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Microsoft Authentication provider as default method (#78)
* Pulled changes from PR 2850 and 2863 * fixed the browserify issue * changes from upstream for git.d.ts and api.d.ts * Implemented Auth using microsoft provider * removed redundant info * fixed issues with signout and signin * Updated version to 0.2.0 --------- Co-authored-by: Ankit Sinha <[email protected]>
Showing
28 changed files
with
1,640 additions
and
218 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -353,4 +353,5 @@ preview-src | |
|
||
.eslintcache | ||
.eslintcache.browser | ||
*.tsbuildinfo | ||
*.tsbuildinfo | ||
.vscode-test-web |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
#!/usr/bin/env node | ||
export declare type BrowserType = 'chromium' | 'firefox' | 'webkit'; | ||
export declare type VSCodeVersion = 'insiders' | 'stable' | 'sources'; | ||
export interface Options { | ||
/** | ||
* Browser to run the test against: 'chromium' | 'firefox' | 'webkit' | ||
*/ | ||
browserType: BrowserType; | ||
/** | ||
* Absolute path to folder that contains one or more extensions (in subfolders). | ||
* Extension folders include a `package.json` extension manifest. | ||
*/ | ||
extensionDevelopmentPath?: string; | ||
/** | ||
* Absolute path to the extension tests runner module. | ||
* Can be either a file path or a directory path that contains an `index.js`. | ||
* The module is expected to have a `run` function of the following signature: | ||
* | ||
* ```ts | ||
* function run(): Promise<void>; | ||
* ``` | ||
* | ||
* When running the extension test, the Extension Development Host will call this function | ||
* that runs the test suite. This function should throws an error if any test fails. | ||
*/ | ||
extensionTestsPath?: string; | ||
/** | ||
* The VS Code version to use. Valid versions are: | ||
* - `'stable'` : The latest stable build | ||
* - `'insiders'` : The latest insiders build | ||
* - `'sources'`: From sources, served at localhost:8080 by running `yarn web` in the vscode repo | ||
* | ||
* Currently defaults to `insiders`, which is latest stable insiders. | ||
*/ | ||
version?: VSCodeVersion; | ||
/** | ||
* Open the dev tools. | ||
*/ | ||
devTools?: boolean; | ||
/** | ||
* Do not show the browser. Defaults to `true` if a extensionTestsPath is provided, `false` otherwise. | ||
*/ | ||
headless?: boolean; | ||
/** | ||
* Expose browser debugging on this port number, and wait for the debugger to attach before running tests. | ||
*/ | ||
waitForDebugger?: number; | ||
/** | ||
* The folder URI to open VSCode on | ||
*/ | ||
folderUri?: string; | ||
} | ||
/** | ||
* Runs the tests in a browser. | ||
* | ||
* @param options The options defining browser type, extension and test location. | ||
*/ | ||
export declare function runTests(options: Options & { | ||
extensionTestsPath: string; | ||
}): Promise<void>; | ||
export declare function open(options: Options): Promise<void>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
exports.existsSync = function (path) { | ||
return false; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import * as vscode from 'vscode'; | ||
import { APIState, GitAPI, GitExtension, PublishEvent } from '../@types/git'; | ||
import { IGit, Repository } from '../api/api'; | ||
import { MockRepository } from './mockRepository'; | ||
|
||
export class MockGitProvider implements IGit, vscode.Disposable { | ||
private _mockRepository: MockRepository; | ||
get repositories(): Repository[] { | ||
return [this._mockRepository]; | ||
} | ||
|
||
get state(): APIState { | ||
return 'initialized'; | ||
} | ||
|
||
private _onDidOpenRepository = new vscode.EventEmitter<Repository>(); | ||
readonly onDidOpenRepository: vscode.Event<Repository> = this._onDidOpenRepository.event; | ||
private _onDidCloseRepository = new vscode.EventEmitter<Repository>(); | ||
readonly onDidCloseRepository: vscode.Event<Repository> = this._onDidCloseRepository.event; | ||
private _onDidChangeState = new vscode.EventEmitter<APIState>(); | ||
readonly onDidChangeState: vscode.Event<APIState> = this._onDidChangeState.event; | ||
private _onDidPublish = new vscode.EventEmitter<PublishEvent>(); | ||
readonly onDidPublish: vscode.Event<PublishEvent> = this._onDidPublish.event; | ||
|
||
private _disposables: vscode.Disposable[]; | ||
|
||
public constructor() { | ||
this._disposables = []; | ||
this._mockRepository = new MockRepository(); | ||
this._mockRepository.addRemote('origin', 'https://anksinha@dev.azure.com/anksinha/test/_git/test'); | ||
this._onDidCloseRepository.fire(this._mockRepository); | ||
this._onDidOpenRepository.fire(this._mockRepository); | ||
} | ||
|
||
dispose() { | ||
this._disposables.forEach(disposable => disposable.dispose()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,305 @@ | ||
import { Uri } from 'vscode'; | ||
import type { | ||
Branch, | ||
BranchQuery, | ||
Change, | ||
Commit, | ||
CommitOptions, | ||
InputBox, | ||
Ref, | ||
Repository, | ||
RepositoryState, | ||
RepositoryUIState, | ||
} from '../api/api'; | ||
import { RefType } from '../api/api1'; | ||
|
||
type Mutable<T> = { | ||
-readonly [P in keyof T]: T[P]; | ||
}; | ||
|
||
export class MockRepository implements Repository { | ||
commit(message: string, opts?: CommitOptions): Promise<void> { | ||
return Promise.reject(new Error(`Unexpected commit(${message}, ${opts})`)); | ||
} | ||
renameRemote(name: string, newName: string): Promise<void> { | ||
return Promise.reject(new Error(`Unexpected renameRemote (${name}, ${newName})`)); | ||
} | ||
getGlobalConfig(key: string): Promise<string> { | ||
return Promise.reject(new Error(`Unexpected getGlobalConfig(${key})`)); | ||
} | ||
detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string | undefined }> { | ||
return Promise.reject(new Error(`Unexpected detectObjectType(${object})`)); | ||
} | ||
buffer(ref: string, path: string): Promise<Buffer> { | ||
return Promise.reject(new Error(`Unexpected buffer(${ref}, ${path})`)); | ||
} | ||
clean(paths: string[]): Promise<void> { | ||
return Promise.reject(new Error(`Unexpected clean(${paths})`)); | ||
} | ||
diffWithHEAD(path?: any): any { | ||
return Promise.reject(new Error(`Unexpected diffWithHEAD(${path})`)); | ||
} | ||
diffIndexWithHEAD(path?: any): any { | ||
return Promise.reject(new Error(`Unexpected diffIndexWithHEAD(${path})`)); | ||
} | ||
diffIndexWith(ref: any, path?: any): any { | ||
return Promise.reject(new Error(`Unexpected diffIndexWith(${ref}, ${path})`)); | ||
} | ||
getMergeBase(ref1: string, ref2: string): Promise<string> { | ||
return Promise.reject(new Error(`Unexpected getMergeBase(${ref1}, ${ref2})`)); | ||
} | ||
log(options?: any): Promise<Commit[]> { | ||
return Promise.reject(new Error(`Unexpected log(${options})`)); | ||
} | ||
|
||
private _state: Mutable<RepositoryState> = { | ||
HEAD: undefined, | ||
refs: [], | ||
remotes: [], | ||
submodules: [], | ||
rebaseCommit: undefined, | ||
mergeChanges: [], | ||
indexChanges: [], | ||
workingTreeChanges: [], | ||
onDidChange: () => ({ dispose() {} }), | ||
}; | ||
private _config: Map<string, string> = new Map(); | ||
private _branches: Branch[] = []; | ||
private _expectedFetches: { remoteName?: string; ref?: string; depth?: number }[] = []; | ||
private _expectedPulls: { unshallow?: boolean }[] = []; | ||
private _expectedPushes: { remoteName?: string; branchName?: string; setUpstream?: boolean }[] = []; | ||
|
||
inputBox: InputBox = { value: '' }; | ||
|
||
rootUri = Uri.file('/root'); | ||
|
||
state: RepositoryState = this._state; | ||
|
||
ui: RepositoryUIState = { | ||
selected: true, | ||
onDidChange: () => ({ dispose() {} }), | ||
}; | ||
|
||
async getConfigs(): Promise<{ key: string; value: string }[]> { | ||
return Array.from(this._config, ([k, v]) => ({ key: k, value: v })); | ||
} | ||
|
||
async getConfig(key: string): Promise<string> { | ||
return this._config.get(key) || ''; | ||
} | ||
|
||
async setConfig(key: string, value: string): Promise<string> { | ||
const oldValue = this._config.get(key) || ''; | ||
this._config.set(key, value); | ||
return oldValue; | ||
} | ||
|
||
getObjectDetails(treeish: string, treePath: string): Promise<{ mode: string; object: string; size: number }> { | ||
return Promise.reject(new Error(`Unexpected getObjectDetails(${treeish}, ${treePath})`)); | ||
} | ||
|
||
show(ref: string, treePath: string): Promise<string> { | ||
return Promise.reject(new Error(`Unexpected show(${ref}, ${treePath})`)); | ||
} | ||
|
||
getCommit(ref: string): Promise<Commit> { | ||
return Promise.reject(new Error(`Unexpected getCommit(${ref})`)); | ||
} | ||
|
||
apply(patch: string, reverse?: boolean | undefined): Promise<void> { | ||
return Promise.reject(new Error(`Unexpected apply(..., ${reverse})`)); | ||
} | ||
|
||
diff(cached?: boolean | undefined): Promise<string> { | ||
return Promise.reject(new Error(`Unexpected diff(${cached})`)); | ||
} | ||
|
||
diffWith(ref: string): Promise<Change[]>; | ||
diffWith(ref: string, treePath: string): Promise<string>; | ||
diffWith(ref: string, treePath?: string) { | ||
return Promise.reject(new Error(`Unexpected diffWith(${ref}, ${treePath})`)); | ||
} | ||
|
||
diffBlobs(object1: string, object2: string): Promise<string> { | ||
return Promise.reject(new Error(`Unexpected diffBlobs(${object1}, ${object2})`)); | ||
} | ||
|
||
diffBetween(ref1: string, ref2: string): Promise<Change[]>; | ||
diffBetween(ref1: string, ref2: string, treePath: string): Promise<string>; | ||
diffBetween(ref1: string, ref2: string, treePath?: string) { | ||
return Promise.reject(new Error(`Unexpected diffBlobs(${ref1}, ${ref2}, ${treePath})`)); | ||
} | ||
|
||
hashObject(data: string): Promise<string> { | ||
return Promise.reject(new Error('Unexpected hashObject(...)')); | ||
} | ||
|
||
async createBranch(name: string, checkout: boolean, ref?: string | undefined): Promise<void> { | ||
if (this._branches.some(b => b.name === name)) { | ||
throw new Error(`A branch named ${name} already exists`); | ||
} | ||
|
||
const branch = { | ||
type: RefType.Head, | ||
name, | ||
commit: ref, | ||
}; | ||
|
||
if (checkout) { | ||
this._state.HEAD = branch; | ||
} | ||
|
||
this._state.refs.push(branch); | ||
this._branches.push(branch); | ||
} | ||
|
||
async deleteBranch(name: string, force?: boolean | undefined): Promise<void> { | ||
const index = this._branches.findIndex(b => b.name === name); | ||
if (index === -1) { | ||
throw new Error(`Attempt to delete nonexistent branch ${name}`); | ||
} | ||
this._branches.splice(index, 1); | ||
} | ||
|
||
async getBranch(name: string): Promise<Branch> { | ||
const branch = this._branches.find(b => b.name === name); | ||
if (!branch) { | ||
throw new Error(`getBranch called with unrecognized name "${name}"`); | ||
} | ||
return branch; | ||
} | ||
|
||
async getBranches(_query: BranchQuery): Promise<Ref[]> { | ||
return []; | ||
} | ||
|
||
async setBranchUpstream(name: string, upstream: string): Promise<void> { | ||
const index = this._branches.findIndex(b => b.name === name); | ||
if (index === -1) { | ||
throw new Error(`setBranchUpstream called with unrecognized branch name ${name})`); | ||
} | ||
|
||
const match = /^refs\/remotes\/([^\/]+)\/(.+)$/.exec(upstream); | ||
if (!match) { | ||
throw new Error( | ||
`upstream ${upstream} provided to setBranchUpstream did match pattern refs/remotes/<name>/<remote-branch>`, | ||
); | ||
} | ||
const [, remoteName, remoteRef] = match; | ||
|
||
const existing = this._branches[index]; | ||
const replacement = { | ||
...existing, | ||
upstream: { | ||
remote: remoteName, | ||
name: remoteRef, | ||
}, | ||
}; | ||
this._branches.splice(index, 1, replacement); | ||
|
||
if (this._state.HEAD === existing) { | ||
this._state.HEAD = replacement; | ||
} | ||
} | ||
|
||
status(): Promise<void> { | ||
return Promise.reject(new Error('Unexpected status()')); | ||
} | ||
|
||
async checkout(treeish: string): Promise<void> { | ||
const branch = this._branches.find(b => b.name === treeish); | ||
|
||
// Also: tags | ||
|
||
if (!branch) { | ||
throw new Error(`checked called with unrecognized ref ${treeish}`); | ||
} | ||
|
||
this._state.HEAD = branch; | ||
} | ||
|
||
async addRemote(name: string, url: string): Promise<void> { | ||
if (this._state.remotes.some(r => r.name === name)) { | ||
throw new Error(`A remote named ${name} already exists.`); | ||
} | ||
|
||
this._state.remotes.push({ | ||
name, | ||
fetchUrl: url, | ||
pushUrl: url, | ||
isReadOnly: false, | ||
}); | ||
} | ||
|
||
async removeRemote(name: string): Promise<void> { | ||
const index = this._state.remotes.findIndex(r => r.name === name); | ||
if (index === -1) { | ||
throw new Error(`No remote named ${name} exists.`); | ||
} | ||
this._state.remotes.splice(index, 1); | ||
} | ||
|
||
async fetch(arg0?: string | undefined | any, ref?: string | undefined, depth?: number | undefined): Promise<void> { | ||
let remoteName: string | undefined; | ||
if (typeof arg0 === 'object') { | ||
remoteName = arg0.remote; | ||
ref = arg0.ref; | ||
depth = arg0.depth; | ||
} else { | ||
remoteName = arg0; | ||
} | ||
|
||
const index = this._expectedFetches.findIndex(f => f.remoteName === remoteName && f.ref === ref && f.depth === depth); | ||
if (index === -1) { | ||
throw new Error(`Unexpected fetch(${remoteName}, ${ref}, ${depth})`); | ||
} | ||
|
||
if (ref) { | ||
const match = /^(?:\+?[^:]+\:)?(.*)$/.exec(ref); | ||
if (match) { | ||
const [, localRef] = match; | ||
await this.createBranch(localRef, false); | ||
} | ||
} | ||
|
||
this._expectedFetches.splice(index, 1); | ||
} | ||
|
||
async pull(unshallow?: boolean | undefined): Promise<void> { | ||
const index = this._expectedPulls.findIndex(f => f.unshallow === unshallow); | ||
if (index === -1) { | ||
throw new Error(`Unexpected pull(${unshallow})`); | ||
} | ||
this._expectedPulls.splice(index, 1); | ||
} | ||
|
||
async push( | ||
remoteName?: string | undefined, | ||
branchName?: string | undefined, | ||
setUpstream?: boolean | undefined, | ||
): Promise<void> { | ||
const index = this._expectedPushes.findIndex( | ||
f => f.remoteName === remoteName && f.branchName === branchName && f.setUpstream === setUpstream, | ||
); | ||
if (index === -1) { | ||
throw new Error(`Unexpected push(${remoteName}, ${branchName}, ${setUpstream})`); | ||
} | ||
this._expectedPushes.splice(index, 1); | ||
} | ||
|
||
blame(treePath: string): Promise<string> { | ||
return Promise.reject(new Error(`Unexpected blame(${treePath})`)); | ||
} | ||
|
||
expectFetch(remoteName?: string, ref?: string, depth?: number) { | ||
this._expectedFetches.push({ remoteName, ref, depth }); | ||
} | ||
|
||
expectPull(unshallow?: boolean) { | ||
this._expectedPulls.push({ unshallow }); | ||
} | ||
|
||
expectPush(remoteName?: string, branchName?: string, setUpstream?: boolean) { | ||
this._expectedPushes.push({ remoteName, branchName, setUpstream }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// This file is providing the test runner to use when running extension tests. | ||
import * as vscode from 'vscode'; | ||
require('mocha/mocha'); | ||
import { mockWebviewEnvironment } from '../mocks/mockWebviewEnvironment'; | ||
import { EXTENSION_ID } from '../../constants'; | ||
|
||
async function runAllExtensionTests(testsRoot: string, clb: (error: Error | null, failures?: number) => void): Promise<any> { | ||
// Ensure the dev-mode extension is activated | ||
await vscode.extensions.getExtension(EXTENSION_ID)!.activate(); | ||
|
||
mockWebviewEnvironment.install(global); | ||
|
||
mocha.setup({ | ||
ui: 'bdd', | ||
reporter: undefined | ||
}); | ||
|
||
try { | ||
const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r); | ||
importAll(require.context('../', true, /\.test$/)); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
|
||
if (process.env.TEST_JUNIT_XML_PATH) { | ||
mocha.reporter('mocha-multi-reporters', { | ||
reporterEnabled: 'mocha-junit-reporter, spec', | ||
mochaJunitReporterReporterOptions: { | ||
mochaFile: process.env.TEST_JUNIT_XML_PATH, | ||
suiteTitleSeparatedBy: ' / ', | ||
outputs: true, | ||
}, | ||
}); | ||
} | ||
|
||
return mocha.run(failures => clb(null, failures)); | ||
} | ||
|
||
export function run(testsRoot: string, clb: (error: Error | null, failures?: number) => void): void { | ||
runAllExtensionTests(testsRoot, clb); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import * as path from 'path'; | ||
import { BrowserType, runTests } from '@vscode/test-web'; | ||
|
||
async function go() { | ||
try { | ||
const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); | ||
const extensionTestsPath = path.resolve(__dirname, './index'); | ||
console.log(extensionDevelopmentPath, extensionTestsPath); | ||
const attachArgName = '--waitForDebugger='; | ||
const waitForDebugger = process.argv.find(arg => arg.startsWith(attachArgName)); | ||
const browserTypeName = '--browserType='; | ||
const browserType = process.argv.find(arg => arg.startsWith(browserTypeName)); | ||
|
||
/** | ||
* Basic usage | ||
*/ | ||
await runTests({ | ||
browserType: browserType ? <BrowserType>browserType.slice(browserTypeName.length) : 'chromium', | ||
extensionDevelopmentPath, | ||
extensionTestsPath, | ||
waitForDebugger: waitForDebugger ? Number(waitForDebugger.slice(attachArgName.length)) : undefined, | ||
}); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
} | ||
|
||
go(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters