From 4f41038046001cfa6b791a01cd850f65e09b77f2 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 16 Sep 2022 17:11:11 -0700 Subject: [PATCH 001/104] update contributors --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b4174fd7a..e1172b55f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ Primary contributors: * [Reid Holmes](https://www.cs.ubc.ca/~rtholmes/) * [Nick Bradley](https://nickbradley.github.io/) - +* [Andrew Stec](https://github.com/andrewstec/) +* [Braxton Hall](https://http://braxtonhall.ca/) + ## Contributing to Classy Features that add value to Classy should be merged back into the Classy project. Any feature that is practical, improves the administration of a course, and is useful to instructors is likely to add value and be accepted as core code. Features, on the other hand, that are only useful to a single course will likely not be accepted as core code. A feature needs to have adequate code coverage (> 90%) and have been tested for a semester in a downstream fork to be eligible as core code. Bug fixes always have value and can be merged via PR back to `ubccpsc/classy` as required. From 0812e70feb5ec9ed6e3a41a647aa3e8992fbba0e Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 16 Sep 2022 17:17:57 -0700 Subject: [PATCH 002/104] Improve grade histogram alignmentImproveprove grade histogram --- .../frontend/src/app/views/AdminGradesTab.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/portal/frontend/src/app/views/AdminGradesTab.ts b/packages/portal/frontend/src/app/views/AdminGradesTab.ts index fa2278b0a..e2ab8873f 100644 --- a/packages/portal/frontend/src/app/views/AdminGradesTab.ts +++ b/packages/portal/frontend/src/app/views/AdminGradesTab.ts @@ -178,7 +178,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: true, sortDown: false, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: 'avg', @@ -186,7 +186,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). defaultSort: false, // Whether the column is the default sort for the table. should only be true for one column. sortDown: false, // Whether the column should initially sort descending or ascending. - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: 'median', @@ -194,7 +194,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '009', @@ -202,7 +202,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '1019', @@ -210,7 +210,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '2029', @@ -218,7 +218,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '3039', @@ -226,7 +226,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '4049', @@ -234,7 +234,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '5059', @@ -242,7 +242,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '6069', @@ -250,7 +250,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '7079', @@ -258,7 +258,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '8089', @@ -266,7 +266,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '9099', @@ -274,7 +274,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '100', @@ -282,7 +282,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' } ]; From 2610b064f8cdd5f7ec262fb5d29e8fd12879677c Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sun, 18 Sep 2022 13:36:11 -0700 Subject: [PATCH 003/104] Improve documentation for configuring development environment. --- .env.sample | 7 ++++++- docs/developer/bootstrap.md | 20 ++++++++++++++++---- packages/portal/frontend/README.md | 5 +++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.env.sample b/.env.sample index d10b0303c..d9f7f93e6 100644 --- a/.env.sample +++ b/.env.sample @@ -60,7 +60,10 @@ DB_URL=mongodb://mongoadmin:strongpasswd@localhost:27017/?authMechanism=DEFAULT ## A GitHub token so the bot can use the GitHub API without going ## through authentication. It is important that this token be well ## protected as without it you can lose programmatic access to student -## projects. The format should be: +## projects. The token can be generated in GitHub by going to the user +## who owns the account, visiting their personal profile page, and +## using the developer option on the side panel. +# The format should be: ## GH_BOT_TOKEN=token d4951x.... ## (yes the word token is required) ## If you want to use ubcbot, contact Reid Holmes for a token. @@ -178,6 +181,8 @@ BACKEND_PORT=3000 ##### ## Full path to fullchain.pem (Can be self-signed for localhost testing) +## localhost version: sudo openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -subj "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN=localhost" -keyout privkey.pem -out fullchain.pem +## localhost certs can be installed in classy/ (or anywhere else) SSL_CERT_PATH=/etc/ssl/fullchain.pem ## Full path to privkey.pem (Can be self-signed for localhost testing) SSL_KEY_PATH=/etc/ssl/privkey.pem diff --git a/docs/developer/bootstrap.md b/docs/developer/bootstrap.md index 8a93e6872..8cb010191 100644 --- a/docs/developer/bootstrap.md +++ b/docs/developer/bootstrap.md @@ -6,8 +6,8 @@ Although Classy is containerized, configuring your development instance does not The software dependencies that are currently used in production and recommended to work in development: -- Node JS > v12.13.0 [Download](https://nodejs.org/en/download/) -- Yarn v1.19.1 [Installation](https://yarnpkg.com/lang/en/docs/install) +- Node JS > v12.13.0 < v13 [Download](https://nodejs.org/en/download/) (or use `nvm`) +- Yarn v1.19.1+ [Installation](https://yarnpkg.com/lang/en/docs/install) - Docker v19.03.4, build 9013bf583a [Install](https://docs.docker.com/install/) - IDE: JetBrain's Webstorm is recommended; VSCode is supported - MongoDB > 3.6.7 (Docker: `docker run -p 27017:27017 mongo`, or [Install](https://docs.mongodb.com/manual/installation/)) @@ -24,12 +24,12 @@ The sample configuration file includes a lot of documentation inline so [take a Classy manages administrators using GitHub teams. The GitHub organization that the course uses should have a `staff` and `admin` team. GitHub users on the `staff` and `admin` teams will have access to the Classy Admin Portal, although users on the `staff` team will have greater privileges (e.g., the ability to configure the course). The bot user should be added as an owner of the organization. -## Install/Build/Run +## Install/Build To install Classy for development: 1. Type `git clone https://github.com/ORGNAME/classy` -2. `cd Classy` to navigate inside the directory. +2. `cd classy` to navigate inside the directory. 3. Inside the directory, type `yarn install` to fetch library dependencies. 4. Then type `yarn run build` to build the project. @@ -37,6 +37,18 @@ To install Classy for development: 5. You are ready to run any of the applications (commands found in `package.json` files under respective application package directories). +## Running for dev + +There are a variety of services you may want to run independently while developing. +Most will require configuring mongo to run in dev mode (see `DB_URL` in `.env`). +The most common of these services can be invoked from the `classy/` directory through either the terminal or IDE: + +* Classy backend: `node -r tsconfig-paths/register packages/portal/backend/src/Backend.js` +* Classy frontend: Instructions in `packages/portal/frontend/README.md` +* Autotest backend: `node packages/autotest/src/AutoTestDaemon.js` + +Some handy dev scripts also exist; these can be found in `portal/backend/src-util/`; use these with care, many modify the database or GitHub repos in unrecoverable ways. + ## QA Checklist More checks may need to be made depending on the nature of your work, but these are the recommended checks: diff --git a/packages/portal/frontend/README.md b/packages/portal/frontend/README.md index 9708c280f..2df2baa12 100644 --- a/packages/portal/frontend/README.md +++ b/packages/portal/frontend/README.md @@ -11,6 +11,7 @@ This assumes you're working with WebStorm. 2) Run WebPack (bundles TS into JS for the browser): `webpack --watch` -3) Start `portal/backend` (probably in WebStorm so you can set breakpoints etc). +3) Start `portal/backend` (probably in WebStorm so you can set breakpoints etc); details about configuring/running the backend can be found in `docs/developer/bootstrap.md` -4) Navigate to `https://localhost:3000` in your browser. +4) Navigate to `https://localhost:3000` in your browser. This may require the `chrome://flags/#allow-insecure-localhost` be enabled, if you are using Chrome. + From b036414fb2087bb6d8f14109114a355b342c0226 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sun, 18 Sep 2022 17:02:17 -0700 Subject: [PATCH 004/104] GitHub changed how the teams API works (https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/); this changes from accessing teams by id (which was awkward) to just using the teamName slug as recommended by the new API. --- .../portal/backend/src-util/GitHubCleaner.ts | 5 +- .../src/controllers/AdminController.ts | 5 +- .../backend/src/controllers/GitHubActions.ts | 370 +++++++++--------- .../test/controllers/AdminControllerSpec.ts | 111 +++--- .../test/controllers/GitHubActionSpec.ts | 126 +++--- .../backend/test/server/AdminRoutesSpec.ts | 170 ++++---- 6 files changed, 410 insertions(+), 377 deletions(-) diff --git a/packages/portal/backend/src-util/GitHubCleaner.ts b/packages/portal/backend/src-util/GitHubCleaner.ts index d9d2ba2e4..236d8b699 100644 --- a/packages/portal/backend/src-util/GitHubCleaner.ts +++ b/packages/portal/backend/src-util/GitHubCleaner.ts @@ -59,8 +59,9 @@ export class GitHubCleaner { Log.info("GitHubCleaner::cleanTeams() - DRY_RUN === false"); for (const team of teamsToRemove) { Log.info("GitHubCleaner::cleanTeams() - removing: " + team.teamName); - const teamNum = await this.tc.getTeamNumber(team.teamName); // await this.gha.getTeamNumber(team.teamName); - await this.gha.deleteTeam(teamNum); + // const teamNum = await this.tc.getTeamNumber(team.teamName); // await this.gha.getTeamNumber(team.teamName); + // await this.gha.deleteTeam(teamNum); + await this.gha.deleteTeam(team.teamName); Log.info("GitHubCleaner::cleanTeams() - done removing: " + team.teamName); } } diff --git a/packages/portal/backend/src/controllers/AdminController.ts b/packages/portal/backend/src/controllers/AdminController.ts index 5674d96a0..44c41ce06 100644 --- a/packages/portal/backend/src/controllers/AdminController.ts +++ b/packages/portal/backend/src/controllers/AdminController.ts @@ -373,8 +373,9 @@ export class AdminController { Log.info("AdminController::performStudentWithdraw() - start"); const gha = GitHubActions.getInstance(true); const tc = new TeamController(); - const teamNum = await tc.getTeamNumber('students'); // await gha.getTeamNumber('students'); - const registeredGithubIds = await gha.getTeamMembers(teamNum); + // const teamNum = await tc.getTeamNumber('students'); // await gha.getTeamNumber('students'); + // const registeredGithubIds = await gha.getTeamMembers(teamNum); + const registeredGithubIds = await gha.getTeamMembers('students'); if (registeredGithubIds.length > 0) { const pc = new PersonController(); diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index 6b2ee5327..d5a2250f9 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -53,10 +53,12 @@ export interface IGitHubActions { /** * Deletes a team. + * NOTE: this used to take a number, but GitHub deprecated this API: + * https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/ * - * @param teamId + * @param teamName: string */ - deleteTeam(teamId: number): Promise; + deleteTeam(teamName: string): Promise; /** * Deletes a team. @@ -71,7 +73,7 @@ export interface IGitHubActions { * This is just a subset of the return, but it is the subset we actually use: * @returns {Promise<{ id: number, name: string, url: string }[]>} */ - listRepos(): Promise>; + listRepos(): Promise>; /** * Gets all people in an org. @@ -79,7 +81,7 @@ export interface IGitHubActions { * @returns {Promise<{ id: number, type: string, url: string, name: string }[]>} * this is just a subset of the return, but it is the subset we actually use */ - listPeople(): Promise>; + listPeople(): Promise>; /** * Lists the teams for the current org. @@ -88,7 +90,7 @@ export interface IGitHubActions { * * @returns {Promise<{id: number, name: string}[]>} */ - listTeams(): Promise>; + listTeams(): Promise>; /** * Lists the Github IDs of members for a teamName (e.g. students). @@ -113,7 +115,7 @@ export interface IGitHubActions { * @param permission 'admin', 'pull', 'push' // admin for staff, push for students * @returns {Promise} team id */ - createTeam(teamName: string, permission: string): Promise<{teamName: string, githubTeamNumber: number, URL: string}>; + createTeam(teamName: string, permission: string): Promise<{ teamName: string, githubTeamNumber: number, URL: string }>; /** * Add a list of Github members (their usernames) to a given team. @@ -156,13 +158,15 @@ export interface IGitHubActions { /** * Gets the list of users on a team. + * NOTE: this used to take a number, but GitHub changed the team API in 2020. + * https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/ * * Returns [] if the team does not exist or nobody is on the team. * - * @param {string} teamNumber + * @param {string} teamName * @returns {Promise} */ - getTeamMembers(teamNumber: number): Promise; + getTeamMembers(teamName: string): Promise; isOnAdminTeam(userName: string): Promise; @@ -249,6 +253,7 @@ export class GitHubActions implements IGitHubActions { this.gitHubUserName = Config.getInstance().getProp(ConfigKey.githubBotName); this.gitHubAuthToken = Config.getInstance().getProp(ConfigKey.githubBotToken); this.dc = DatabaseController.getInstance(); + Log.trace("GitHubActions:: - url: " + this.apiPath + "/" + this.org); } private static instance: IGitHubActions = null; @@ -315,20 +320,20 @@ export class GitHubActions implements IGitHubActions { const uri = this.apiPath + '/orgs/' + this.org + '/repos'; const options: RequestInit = { - method: 'POST', + method: 'POST', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' }, - body: JSON.stringify({ - name: repoId, + body: JSON.stringify({ + name: repoId, // In Dev and Test, Github free Org Repos cannot be private. - private: true, - has_issues: true, - has_wiki: false, + private: true, + has_issues: true, + has_wiki: false, has_downloads: false, - auto_init: false + auto_init: false }) }; @@ -374,11 +379,11 @@ export class GitHubActions implements IGitHubActions { const uri = this.apiPath + '/repos/' + this.org + '/' + repoName; Log.trace("GitHubAction::deleteRepo( " + repoName + " ) - URI: " + uri); const options: RequestInit = { - method: 'DELETE', + method: 'DELETE', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; @@ -408,11 +413,11 @@ export class GitHubActions implements IGitHubActions { const start = Date.now(); const uri = this.apiPath + '/repos/' + this.org + '/' + repoName; const options: RequestInit = { - method: 'GET', + method: 'GET', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; @@ -427,43 +432,47 @@ export class GitHubActions implements IGitHubActions { public async deleteTeamByName(teamName: string): Promise { Log.info("GitHubAction::deleteTeamByName( " + this.org + ", " + teamName + " ) - start"); - const teamNum = await this.getTeamNumber(teamName); // be conservative, don't use TeamController on purpose - if (teamNum >= 0) { - return await this.deleteTeam(teamNum); - } - return false; + // const teamNum = await this.getTeamNumber(teamName); // be conservative, don't use TeamController on purpose + // if (teamNum >= 0) { + return await this.deleteTeam(teamName); + // } + // return false; } /** * Deletes a team from GitHub. Does _NOT_ modify the Team object in the database. + * NOTE: this used to take a teamId: number, but GitHub deprecated this API: + * https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/ * * NOTE: if you're deleting the 'admin', 'staff', or 'students' teams, you're doing something terribly wrong. * * @param teamId */ - public async deleteTeam(teamId: number): Promise { + public async deleteTeam(teamName: string): Promise { try { const start = Date.now(); - Log.info("GitHubAction::deleteTeam( " + teamId + " ) - start"); + Log.info("GitHubAction::deleteTeam( " + teamName + " ) - start"); - if (teamId === null) { + if (teamName === null) { throw new Error("GitHubAction::deleteTeam( null ) - null team requested"); } - if (teamId === -1) { - Log.info("GitHubAction::deleteTeam( " + teamId + " ) - team does not exist, not deleting; took: " + Util.took(start)); + if (teamName === null || teamName.length < 1) { + Log.info("GitHubAction::deleteTeam( " + teamName + " ) - team does not exist, not deleting; took: " + Util.took(start)); return false; } - const uri = this.apiPath + '/teams/' + teamId; + // const uri = this.apiPath + '/teams/' + teamId; + // DELETE /orgs/:org/teams/:team_slug + const uri = this.apiPath + "/orgs/" + this.org + "/teams/" + teamName; const options: RequestInit = { - method: 'DELETE', - headers: { + method: 'DELETE', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, + 'User-Agent': this.gitHubUserName, // 'Accept': 'application/json', // custom because this is a preview api - 'Accept': 'application/vnd.github.hellcat-preview+json' + 'Accept': 'application/vnd.github.hellcat-preview+json' } }; @@ -491,7 +500,7 @@ export class GitHubActions implements IGitHubActions { * This is just a subset of the return, but it is the subset we actually use: * @returns {Promise<{ id: number, name: string, url: string }[]>} */ - public async listRepos(): Promise> { + public async listRepos(): Promise> { Log.info("GitHubActions::listRepos(..) - start"); const start = Date.now(); @@ -499,17 +508,17 @@ export class GitHubActions implements IGitHubActions { const uri = this.apiPath + '/orgs/' + this.org + '/repos?per_page=' + this.pageSize; Log.trace("GitHubActions::listRepos(..) - URI: " + uri); const options: RequestInit = { - method: 'GET', - headers: { + method: 'GET', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; const raw: any = await this.handlePagination(uri, options); - const rows: Array<{repoName: string, repoNumber: number, url: string}> = []; + const rows: Array<{ repoName: string, repoNumber: number, url: string }> = []; for (const entry of raw) { const id = entry.id; const name = entry.name; @@ -528,24 +537,24 @@ export class GitHubActions implements IGitHubActions { * @returns {Promise<{ id: number, type: string, url: string, name: string }[]>} * this is just a subset of the return, but it is the subset we actually use */ - public async listPeople(): Promise> { + public async listPeople(): Promise> { Log.info("GitHubActions::listPeople(..) - start"); const start = Date.now(); // GET /orgs/:org/members const uri = this.apiPath + '/orgs/' + this.org + '/members?per_page=' + this.pageSize; const options: RequestInit = { - method: 'GET', - headers: { + method: 'GET', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; const raw: any = await this.handlePagination(uri, options); - const rows: Array<{githubId: string, personNumber: number, url: string}> = []; + const rows: Array<{ githubId: string, personNumber: number, url: string }> = []; for (const entry of raw) { const id = entry.id; const url = entry.html_url; @@ -575,16 +584,16 @@ export class GitHubActions implements IGitHubActions { raw = body; let lastPage: number = -1; - const linkText = response.headers.get('link'); - // Log.trace('GitHubActions::handlePagination(..) - linkText: ' + linkText); + const linkText = response.headers.get('link'); + Log.trace('GitHubActions::handlePagination(..) - linkText: ' + linkText); const linkParts = linkText.split(','); for (const p of linkParts) { const pparts = p.split(';'); if (pparts[1].indexOf('last')) { const pText = pparts[0].split('&page=')[1]; - // Log.trace('GitHubActions::handlePagination(..) - last page pText:_' + pText + '_; p: ' + p); + Log.trace('GitHubActions::handlePagination(..) - last page pText:_' + pText + '_; p: ' + p); lastPage = Number(pText.match(/\d+/)[0]); - // Log.trace('GitHubActions::handlePagination(..) - last page: ' + lastPage); + Log.trace('GitHubActions::handlePagination(..) - last page: ' + lastPage); } } @@ -593,30 +602,30 @@ export class GitHubActions implements IGitHubActions { const pparts = p.split(';'); if (pparts[1].indexOf('next')) { let pText = pparts[0].split('&page=')[0].trim(); - // Log.trace('GitHubActions::handlePagination(..) - pt: ' + pText); + Log.trace('GitHubActions::handlePagination(..) - pt: ' + pText); pText = pText.substring(1); pText = pText + "&page="; pageBase = pText; - // Log.trace('GitHubActions::handlePagination(..) - page base: ' + pageBase); + Log.trace('GitHubActions::handlePagination(..) - page base: ' + pageBase); } } - // Log.trace("GitHubActions::handlePagination(..) - handling pagination; # pages: " + lastPage); + Log.trace("GitHubActions::handlePagination(..) - handling pagination; # pages: " + lastPage); for (let i = 2; i <= lastPage; i++) { const pageUri = pageBase + i; - // Log.trace('GitHubActions::handlePagination(..) - page to request: ' + pageUri); + Log.trace('GitHubActions::handlePagination(..) - page to request: ' + pageUri); uri = pageUri; // not sure why this is needed // NOTE: this needs to be slowed down to prevent DNS problems (issuing 10+ concurrent dns requests can be problematic) await Util.delay(100); paginationPromises.push(fetch(uri, options as any)); } } else { - // Log.trace("GitHubActions::handlePagination(..) - single page"); + Log.trace("GitHubActions::handlePagination(..) - single page"); raw = body; // don't put anything on the paginationPromise if it isn't paginated } - // Log.trace("GitHubActions::handlePagination(..) - requesting all"); + Log.trace("GitHubActions::handlePagination(..) - requesting all"); // this block won't do anything if we just did the raw thing above (aka no pagination) const responses: any[] = await Promise.all(paginationPromises); // Log.trace("GitHubActions::handlePagination(..) - requests complete"); @@ -636,11 +645,11 @@ export class GitHubActions implements IGitHubActions { /** * Lists the teams for the current org. * - * NOTE: this is a slow operation (if there are many teams) so try not to do it too much! + * NOTE: this is a slow operation (if there are many teams) so try not to do it too often! * * @returns {Promise<{id: number, name: string}[]>} */ - public async listTeams(): Promise> { + public async listTeams(): Promise> { // Log.info("GitHubActions::listTeams(..) - start"); const start = Date.now(); @@ -648,18 +657,18 @@ export class GitHubActions implements IGitHubActions { const uri = this.apiPath + '/orgs/' + this.org + '/teams?per_page=' + this.pageSize; Log.info("GitHubActions::listTeams(..) - start"); // uri: " + uri); const options: RequestInit = { - method: 'GET', - headers: { + method: 'GET', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, + 'User-Agent': this.gitHubUserName, // 'Accept': 'application/json', - 'Accept': 'application/vnd.github.hellcat-preview+json' + 'Accept': 'application/vnd.github.hellcat-preview+json' } }; const teamsRaw: any = await this.handlePagination(uri, options); - const teams: Array<{teamName: string, teamNumber: number}> = []; + const teams: Array<{ teamName: string, teamNumber: number }> = []; for (const team of teamsRaw) { const teamNumber = team.id; const teamName = team.name; @@ -676,10 +685,10 @@ export class GitHubActions implements IGitHubActions { // POST /repos/:owner/:repo/hooks const uri = this.apiPath + '/repos/' + this.org + '/' + repoName + '/hooks'; const opts: RequestInit = { - method: 'GET', + method: 'GET', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName + 'User-Agent': this.gitHubUserName } }; @@ -702,18 +711,18 @@ export class GitHubActions implements IGitHubActions { // POST /repos/:owner/:repo/hooks const uri = this.apiPath + '/repos/' + this.org + '/' + repoName + '/hooks'; const opts: RequestInit = { - method: 'POST', + method: 'POST', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName + 'User-Agent': this.gitHubUserName }, - body: JSON.stringify({ - name: "web", + body: JSON.stringify({ + name: "web", active: true, events: ["commit_comment", "push"], config: { - url: webhookEndpoint, - secret: secret, + url: webhookEndpoint, + secret: secret, content_type: "json" } }) @@ -743,18 +752,18 @@ export class GitHubActions implements IGitHubActions { // PATCH /repos/:owner/:repo/hooks/:hook_id const uri = this.apiPath + '/repos/' + this.org + '/' + repoName + '/hooks/' + hookId; const opts: RequestInit = { - method: 'PATCH', + method: 'PATCH', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName + 'User-Agent': this.gitHubUserName }, - body: JSON.stringify({ - name: "web", + body: JSON.stringify({ + name: "web", active: true, events: ["commit_comment", "push"], config: { - url: webhookEndpoint, - secret: secret, + url: webhookEndpoint, + secret: secret, content_type: "json" } }) @@ -779,7 +788,7 @@ export class GitHubActions implements IGitHubActions { * @param permission 'admin', 'pull', 'push' // admin for staff, push for students * @returns {Promise} team id */ - public async createTeam(teamName: string, permission: string): Promise<{teamName: string, githubTeamNumber: number, URL: string}> { + public async createTeam(teamName: string, permission: string): Promise<{ teamName: string, githubTeamNumber: number, URL: string }> { Log.info("GitHubAction::teamCreate( " + this.org + ", " + teamName + ", " + permission + ", ... ) - start"); const start = Date.now(); @@ -796,14 +805,14 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubAction::teamCreate( ' + teamName + ', ... ) - does not exist; creating'); const uri = this.apiPath + '/orgs/' + this.org + '/teams'; const options: RequestInit = { - method: 'POST', + method: 'POST', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' }, - body: JSON.stringify({ - name: teamName, + body: JSON.stringify({ + name: teamName, permission: permission }) }; @@ -863,11 +872,11 @@ export class GitHubActions implements IGitHubActions { const uri = this.apiPath + '/teams/' + teamNumber + '/memberships/' + member; Log.info("GitHubAction::addMembersToTeam(..) - uri: " + uri); const opts: RequestInit = { - method: 'PUT', + method: 'PUT', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; promises.push(fetch(uri, opts)); @@ -914,11 +923,11 @@ export class GitHubActions implements IGitHubActions { const uri = this.apiPath + '/teams/' + teamNumber + '/memberships/' + member; Log.info("GitHubAction::removeMembersFromTeam(..) - uri: " + uri); const opts: RequestInit = { - method: 'DELETE', + method: 'DELETE', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; promises.push(fetch(uri, opts)); @@ -946,14 +955,14 @@ export class GitHubActions implements IGitHubActions { const uri = this.apiPath + '/teams/' + teamId + '/repos/' + this.org + '/' + repoName; // Log.info("GitHubAction::addTeamToRepo(..) - URI: " + uri); const options: RequestInit = { - method: 'PUT', + method: 'PUT', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' // 'Accept': 'application/vnd.github.hellcat-preview+json' }, - body: JSON.stringify({ + body: JSON.stringify({ permission: permission }) }; @@ -1013,25 +1022,29 @@ export class GitHubActions implements IGitHubActions { * * Returns [] if the team does not exist or nobody is on the team. * - * @param {string} teamNumber - * @returns {Promise} + * @param {string} teamName + * @returns {Promise} */ - public async getTeamMembers(teamNumber: number): Promise { - Log.info("GitHubAction::getTeamMembers( " + teamNumber + " ) - start"); + public async getTeamMembers(teamName: string): Promise { + // public async getTeamMembers(teamNumber: number): Promise { + Log.info("GitHubAction::getTeamMembers( " + teamName + " ) - start"); - if (teamNumber === null) { + if (teamName === null) { throw new Error("GitHubAction::getTeamMembers( null ) - null team requested"); } const start = Date.now(); try { - const uri = this.apiPath + '/teams/' + teamNumber + '/members?per_page=' + this.pageSize; + // deprecated: https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/ + // const uri = this.apiPath + '/teams/' + teamNumber + '/members?per_page=' + this.pageSize; + // /orgs/:org/teams/:team_slug + const uri = this.apiPath + "/orgs/" + this.org + "/teams/" + teamName + "/members"; const options: RequestInit = { - method: 'GET', - headers: { + method: 'GET', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; @@ -1080,11 +1093,11 @@ export class GitHubActions implements IGitHubActions { const start = Date.now(); const uri = this.apiPath + '/teams/' + teamNumber; const options: RequestInit = { - method: 'GET', - headers: { + method: 'GET', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; @@ -1123,11 +1136,9 @@ export class GitHubActions implements IGitHubActions { } const tc = new TeamController(); - const teamNumber = await tc.getTeamNumber(teamName); // try to use cache - + // const teamNumber = await tc.getTeamNumber(teamName); // try to use cache // const teamNumber = await gh.getTeamNumber(teamName); - - const teamMembers = await gh.getTeamMembers(teamNumber); + const teamMembers = await gh.getTeamMembers(teamName); for (const member of teamMembers) { if (member === userName) { Log.info('GitHubAction::isOnTeam(..) - user: ' + userName + @@ -1145,8 +1156,9 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubAction::listTeamMembers( ' + teamName + ' ) - start'); const gh = this; - const teamNumber = await new TeamController().getTeamNumber(teamName); - const teamMembers = await gh.getTeamMembers(teamNumber); + // const teamNumber = await new TeamController().getTeamNumber(teamName); + // const teamMembers = await gh.getTeamMembers(teamNumber); + const teamMembers = await gh.getTeamMembers(teamName); return teamMembers; } @@ -1285,7 +1297,7 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubActions::importRepoFS(..)::moveFiles( ' + originPath + ', ' + filesLocation + ', ' + destPath + ') - moving files'); return exec(`cp -r ${originPath}/${filesLocation} ${destPath}`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::moveFiles(..) - done'); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::moveFiles(..)'); that.reportStdErr(result.stderr, 'importRepoFS(..)::moveFiles(..)'); @@ -1295,7 +1307,7 @@ export class GitHubActions implements IGitHubActions { function cloneRepo(repoPath: string) { Log.info('GitHubActions::importRepoFS(..)::cloneRepo() - cloning: ' + importRepo); return exec(`git clone -q ${authedImportRepo} ${repoPath}`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::cloneRepo() - done'); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::cloneRepo()'); that.reportStdErr(result.stderr, 'importRepoFS(..)::cloneRepo()'); @@ -1306,7 +1318,7 @@ export class GitHubActions implements IGitHubActions { if (typeof branch === "string" && branch !== "") { Log.info(`GitHubActions::importRepoFS(..)::checkout() - Checking out "${branch}"`); return exec(`cd ${repoPath} && git checkout ${branch}`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::checkout() - done'); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::checkout()'); that.reportStdErr(result.stderr, 'importRepoFS(..)::checkout()'); @@ -1320,7 +1332,7 @@ export class GitHubActions implements IGitHubActions { function removeGitDir() { Log.info('GitHubActions::importRepoFS(..)::removeGitDir() - removing .git from cloned repo'); return exec(`cd ${cloneTempDir.path} && rm -rf .git`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::removeGitDir() - done'); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::removeGitDir()'); that.reportStdErr(result.stderr, 'importRepoFS(..)::removeGitDir()'); @@ -1330,7 +1342,7 @@ export class GitHubActions implements IGitHubActions { function initGitDir() { Log.info('GitHubActions::importRepoFS(..)::initGitDir() - start'); return exec(`cd ${cloneTempDir.path} && git init -q && git branch -m main`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::initGitDir() - done'); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::initGitDir()'); that.reportStdErr(result.stderr, 'importRepoFS(..)::initGitDir()'); @@ -1341,7 +1353,7 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubActions::importRepoFS(..)::changeGitRemote() - start'); const command = `cd ${cloneTempDir.path} && git remote add origin ${authedStudentRepo}.git && git fetch --all -q`; return exec(command) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::changeGitRemote() - done'); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::changeGitRemote()'); that.reportStdErr(result.stderr, 'importRepoFS(..)::changeGitRemote()'); @@ -1353,7 +1365,7 @@ export class GitHubActions implements IGitHubActions { // tslint:disable-next-line const command = `cd ${cloneTempDir.path} && git config user.email "classy@cs.ubc.ca" && git config user.name "classy" && git add . && git commit -q -m "Starter files"`; return exec(command) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::addFilesToRepo() - done'); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::addFilesToRepo()'); that.reportStdErr(result.stderr, 'importRepoFS(..)::addFilesToRepo()'); @@ -1365,7 +1377,7 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubActions::importRepoFS(..)::pushToNewRepo() - start'); const command = `cd ${cloneTempDir.path} && git push -q origin main`; return exec(command) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::importRepoFS(..)::pushToNewRepo() - done; took: ' + Util.took(pushStart)); that.reportStdOut(result.stdout, 'GitHubActions::importRepoFS(..)::pushToNewRepo()'); that.reportStdErr(result.stderr, 'importRepoFS(..)::pushToNewRepo()'); @@ -1460,7 +1472,7 @@ export class GitHubActions implements IGitHubActions { const cloneStart = Date.now(); Log.info('GitHubActions::writeFileToRepo(..)::cloneRepo() - cloning: ' + repoURL); return exec(`git clone -q ${authedRepo} ${repoPath}`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::writeFileToRepo(..)::cloneRepo() - done; took: ' + Util.took(cloneStart)); that.reportStdOut(result.stdout, 'GitHubActions::writeFileToRepo(..)::cloneRepo()'); // if (result.stderr) { @@ -1473,7 +1485,7 @@ export class GitHubActions implements IGitHubActions { function enterRepoPath() { Log.info('GitHubActions::writeFileToRepo(..)::enterRepoPath() - entering: ' + tempPath); return exec(`cd ${tempPath}`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::writeFileToRepo(..)::enterRepoPath() - done'); that.reportStdOut(result.stdout, 'GitHubActions::writeFileToRepo(..)::enterRepoPath()'); that.reportStdErr(result.stderr, 'writeFileToRepo(..)::enterRepoPath()'); @@ -1483,7 +1495,7 @@ export class GitHubActions implements IGitHubActions { function createNewFileForce() { Log.info('GitHubActions::writeFileToRepo(..)::createNewFileForce() - writing: ' + fileName); return exec(`cd ${tempPath} && if [ -f ${fileName} ]; then rm ${fileName}; fi; echo '${fileContent}' >> ${fileName};`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::writeFileToRepo(..)::createNewFileForce() - done'); that.reportStdOut(result.stdout, 'GitHubActions::writeFileToRepo(..)::createNewFileForce()'); that.reportStdErr(result.stderr, 'writeFileToRepo(..)::createNewFileForce()'); @@ -1493,7 +1505,7 @@ export class GitHubActions implements IGitHubActions { function createNewFile() { Log.info('GitHubActions::writeFileToRepo(..)::createNewFile() - writing: ' + fileName); return exec(`cd ${tempPath} && if [ ! -f ${fileName} ]; then echo \"${fileContent}\" >> ${fileName};fi`) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::writeFileToRepo(..)::createNewFile() - done'); that.reportStdOut(result.stdout, 'GitHubActions::writeFileToRepo(..)::createNewFile()'); that.reportStdErr(result.stderr, 'writeFileToRepo(..)::createNewFile()'); @@ -1504,7 +1516,7 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubActions::writeFileToRepo(..)::addFilesToRepo() - start'); const command = `cd ${tempPath} && git add ${fileName}`; return exec(command) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::writeFileToRepo(..)::addFilesToRepo() - done'); that.reportStdOut(result.stdout, 'GitHubActions::writeFileToRepo(..)::addFilesToRepo()'); that.reportStdErr(result.stderr, 'writeFileToRepo(..)::addFilesToRepo()'); @@ -1515,7 +1527,7 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubActions::writeFileToRepo(..)::commitFilesToRepo() - start'); const command = `cd ${tempPath} && git commit -q -m "Update ${fileName}"`; return exec(command) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::writeFileToRepo(..)::commitFilesToRepo() - done'); that.reportStdOut(result.stdout, 'GitHubActions::writeFileToRepo(..)::commitFilesToRepo()'); that.reportStdErr(result.stderr, 'writeFileToRepo(..)::commitFilesToRepo()'); @@ -1526,7 +1538,7 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubActions::writeFileToRepo(..)::pushToRepo() - start'); const command = `cd ${tempPath} && git push -q`; return exec(command) - .then(function(result: any) { + .then(function (result: any) { Log.info('GitHubActions::writeFileToRepo(..)::pushToNewRepo() - done'); that.reportStdOut(result.stdout, 'GitHubActions::writeFileToRepo(..)::pushToNewRepo()'); that.reportStdErr(result.stderr, 'writeFileToRepo(..)::pushToNewRepo()'); @@ -1562,11 +1574,11 @@ export class GitHubActions implements IGitHubActions { const teamsUri = this.apiPath + '/repos/' + this.org + '/' + repoName + '/teams'; Log.trace("GitHubAction::setRepoPermission(..) - URI: " + teamsUri); const teamOptions: RequestInit = { - method: 'GET', + method: 'GET', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; @@ -1582,13 +1594,13 @@ export class GitHubActions implements IGitHubActions { const permissionUri = this.apiPath + '/teams/' + team.id + '/repos/' + this.org + '/' + repoName; Log.trace("GitHubAction::setRepoPermission(..) - URI: " + permissionUri); const permissionOptions: RequestInit = { - method: 'PUT', + method: 'PUT', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' }, - body: JSON.stringify({ + body: JSON.stringify({ permission: permissionLevel }) }; @@ -1632,13 +1644,13 @@ export class GitHubActions implements IGitHubActions { restrictions: null }); const options: RequestInit = { - method: 'PUT', - headers: { + method: 'PUT', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, + 'User-Agent': this.gitHubUserName, // TODO this API is being used in a beta state. Get off the beta! // https://developer.github.com/enterprise/2.19/v3/repos/branches/#update-branch-protection - 'Accept': 'application/vnd.github.luke-cage-preview+json' + 'Accept': 'application/vnd.github.luke-cage-preview+json' }, body }; @@ -1666,11 +1678,11 @@ export class GitHubActions implements IGitHubActions { body: issue.body, }); const options: RequestInit = { - method: 'POST', - headers: { + method: 'POST', + headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' }, body }; @@ -1772,20 +1784,20 @@ export class GitHubActions implements IGitHubActions { const apiUrl = c.getProp(ConfigKey.githubAPI) + '/api/v3/repos/' + c.getProp(ConfigKey.org) + '/' + projectName; const body = { - comment: { + comment: { commit_id: sha, // 82ldl2731c665c364ad979c9135688d1c206462c // tslint:disable-next-line - html_url: repoUrl + "/commit/" + sha + "#fooWillBeStripped", // https://github.ugrad.cs.ubc.ca/CPSC310-2018W-T1/project_r2d2_c3p0/commit/82ldl2731c665c364ad979c9135688d1c206462c#commitcomment-285811" - user: { + html_url: repoUrl + "/commit/" + sha + "#fooWillBeStripped", // https://github.ugrad.cs.ubc.ca/CPSC310-2018W-T1/project_r2d2_c3p0/commit/82ldl2731c665c364ad979c9135688d1c206462c#commitcomment-285811" + user: { login: Config.getInstance().getProp(ConfigKey.botName) // userId // autobot }, - body: message + body: message }, repository: { // tslint:disable-next-line commits_url: apiUrl + '/commits{/sha}', // https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2018W-T1/project_r2d2_c3p0/commits{/sha} - clone_url: repoUrl + ".git", // https://github.ugrad.cs.ubc.ca/CPSC310-2018W-T1/project_r2d2_c3p0.git - name: projectName + clone_url: repoUrl + ".git", // https://github.ugrad.cs.ubc.ca/CPSC310-2018W-T1/project_r2d2_c3p0.git + name: projectName } }; @@ -1793,14 +1805,14 @@ export class GitHubActions implements IGitHubActions { Log.info("GitHubService::simulateWebookComment(..) - url: " + urlToSend + "; body: " + JSON.stringify(body)); const options: RequestInit = { - method: "POST", + method: "POST", headers: { - "Content-Type": "application/json", - "User-Agent": "UBC-AutoTest", + "Content-Type": "application/json", + "User-Agent": "UBC-AutoTest", "X-GitHub-Event": "commit_comment", - "Authorization": Config.getInstance().getProp(ConfigKey.githubBotToken) // TODO: support auth from github + "Authorization": Config.getInstance().getProp(ConfigKey.githubBotToken) // TODO: support auth from github }, - body: JSON.stringify(body) + body: JSON.stringify(body) }; if (Config.getInstance().getProp(ConfigKey.postback) === true) { @@ -1852,13 +1864,13 @@ export class GitHubActions implements IGitHubActions { const body = {body: message}; const options: RequestInit = { - method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": "UBC-AutoTest", + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "UBC-AutoTest", "Authorization": Config.getInstance().getProp(ConfigKey.githubBotToken) }, - body: JSON.stringify(body) + body: JSON.stringify(body) }; Log.trace("GitHubService::makeComment(..) - url: " + url); @@ -1894,11 +1906,11 @@ export class GitHubActions implements IGitHubActions { const start = Date.now(); const uri = this.apiPath + '/repos/' + this.org + '/' + repoId + '/teams'; const options: RequestInit = { - method: 'GET', + method: 'GET', headers: { 'Authorization': this.gitHubAuthToken, - 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'User-Agent': this.gitHubUserName, + 'Accept': 'application/json' } }; @@ -1968,7 +1980,7 @@ export class TestGitHubActions implements IGitHubActions { return this.repos[repoId]; } - public async createTeam(teamName: string, permission: string): Promise<{teamName: string; githubTeamNumber: number; URL: string}> { + public async createTeam(teamName: string, permission: string): Promise<{ teamName: string; githubTeamNumber: number; URL: string }> { if (typeof this.teams[teamName] === 'undefined') { const c = Config.getInstance(); const url = c.getProp(ConfigKey.githubHost) + '/' + c.getProp(ConfigKey.org) + '/teams/' + teamName; @@ -2011,24 +2023,25 @@ export class TestGitHubActions implements IGitHubActions { return false; } - public async deleteTeam(teamId: number): Promise { - Log.info("TestGitHubActions::deleteTeam( " + teamId + " )"); + public async deleteTeam(teamNameToDelete: string): Promise { + Log.info("TestGitHubActions::deleteTeam( " + teamNameToDelete + " )"); for (const teamName of Object.keys(this.teams)) { const team = this.teams[teamName]; - if (team.githubTeamNumber === teamId) { - Log.info("TestGitHubActions::deleteTeam( " + teamId + " ) - deleting team name: " + team.id); + // if (team.githubTeamNumber === teamId) { + if (team.name === teamNameToDelete) { + Log.info("TestGitHubActions::deleteTeam( " + teamNameToDelete + " ) - deleting team name: " + team.name); delete this.teams[teamName]; return true; } } - Log.info("TestGitHubActions::deleteTeam( " + teamId + " ); not deleted"); + Log.info("TestGitHubActions::deleteTeam( " + teamNameToDelete + " ); not deleted"); return false; } - public async getTeamMembers(teamNumber: number): Promise { - Log.info("TestGitHubActions::getTeamMembers( " + teamNumber + " )"); - if (teamNumber < 0) { + public async getTeamMembers(teamName: string): Promise { + Log.info("TestGitHubActions::getTeamMembers( " + teamName + " )"); + if (teamName === null || teamName.length < 1) { return []; } return [Test.REALBOTNAME1, Test.REALUSERNAME, Test.ADMIN1.github]; @@ -2107,7 +2120,7 @@ export class TestGitHubActions implements IGitHubActions { } } - public async listPeople(): Promise> { + public async listPeople(): Promise> { Log.info("TestGitHubActions::listPeople(..)"); const people = []; @@ -2126,7 +2139,7 @@ export class TestGitHubActions implements IGitHubActions { return people; } - public async listRepos(): Promise> { + public async listRepos(): Promise> { Log.info("TestGitHubActions::listRepos(..)"); const ret = []; for (const name of Object.keys(this.repos)) { @@ -2137,13 +2150,14 @@ export class TestGitHubActions implements IGitHubActions { return ret; } + // Map teamName: {teamObject} private teams: any = { staff: {id: TeamController.STAFF_NAME, teamName: TeamController.STAFF_NAME, githubTeamNumber: '1000'}, admin: {id: TeamController.ADMIN_NAME, teamName: TeamController.ADMIN_NAME, githubTeamNumber: '1001'} }; // TODO: use a private teams map to keep track of teams - public async listTeams(): Promise> { + public async listTeams(): Promise> { Log.info("TestGitHubActions::listTeams(..)"); // return [{teamNumber: Date.now(), teamName: Test.TEAMNAME1}]; const ret = []; diff --git a/packages/portal/backend/test/controllers/AdminControllerSpec.ts b/packages/portal/backend/test/controllers/AdminControllerSpec.ts index c127b72b8..19db4748a 100644 --- a/packages/portal/backend/test/controllers/AdminControllerSpec.ts +++ b/packages/portal/backend/test/controllers/AdminControllerSpec.ts @@ -4,7 +4,12 @@ import "mocha"; import Config, {ConfigCourses, ConfigKey} from "../../../../common/Config"; import Log from "../../../../common/Log"; import {Test} from "../../../../common/TestHarness"; -import {AutoTestGradeTransport, GradeTransport, StudentTransport, TeamTransport} from "../../../../common/types/PortalTypes"; +import { + AutoTestGradeTransport, + GradeTransport, + StudentTransport, + TeamTransport +} from "../../../../common/types/PortalTypes"; import {AdminController} from "../../src/controllers/AdminController"; import {ICourseController} from "../../src/controllers/CourseController"; @@ -33,12 +38,12 @@ describe("AdminController", () => { let dc: DeliverablesController; let gha: IGitHubActions; - before(async function() { + before(async function () { await Test.suiteBefore('AdminController'); await clearAndPrepareAll(); }); - beforeEach(async function() { + beforeEach(async function () { gha = GitHubActions.getInstance(true); const ghInstance = new GitHubController(gha); @@ -52,7 +57,7 @@ describe("AdminController", () => { dc = new DeliverablesController(); }); - after(async function() { + after(async function () { Test.suiteAfter('AdminController'); }); @@ -87,18 +92,24 @@ describe("AdminController", () => { // await gha.deleteTeam(teamNum); // NOTE: using GHA instead of TC because we really want to clear out GitHub - let teamNum = await gha.getTeamNumber('t_d0_' + Test.GITHUB1.csId); - await gha.deleteTeam(teamNum); - teamNum = await gha.getTeamNumber('t_d0_' + Test.GITHUB2.csId); - await gha.deleteTeam(teamNum); - teamNum = await gha.getTeamNumber('t_d0_' + Test.GITHUB3.csId); - await gha.deleteTeam(teamNum); - teamNum = await gha.getTeamNumber('t_project_' + Test.GITHUB1.csId + '_' + Test.GITHUB2.csId); - await gha.deleteTeam(teamNum); - teamNum = await gha.getTeamNumber('t_project_' + Test.GITHUB3.csId); - await gha.deleteTeam(teamNum); - teamNum = await gha.getTeamNumber(Test.TEAMNAMEREAL); - await gha.deleteTeam(teamNum); + // let teamNum = await gha.getTeamNumber('t_d0_' + Test.GITHUB1.csId); + // await gha.deleteTeam(teamNum); + await gha.deleteTeam('t_d0_' + Test.GITHUB1.csId); + // teamNum = await gha.getTeamNumber('t_d0_' + Test.GITHUB2.csId); + // await gha.deleteTeam(teamNum); + await gha.deleteTeam('t_d0_' + Test.GITHUB2.csId); + // teamNum = await gha.getTeamNumber('t_d0_' + Test.GITHUB3.csId); + // await gha.deleteTeam(teamNum); + await gha.deleteTeam('t_d0_' + Test.GITHUB3.csId); + // teamNum = await gha.getTeamNumber('t_project_' + Test.GITHUB1.csId + '_' + Test.GITHUB2.csId); + // await gha.deleteTeam(teamNum); + await gha.deleteTeam('t_project_' + Test.GITHUB1.csId + '_' + Test.GITHUB2.csId); + // teamNum = await gha.getTeamNumber('t_project_' + Test.GITHUB3.csId); + // await gha.deleteTeam(teamNum); + await gha.deleteTeam('t_project_' + Test.GITHUB3.csId); + // teamNum = await gha.getTeamNumber(Test.TEAMNAMEREAL); + // await gha.deleteTeam(teamNum); + await gha.deleteTeam(Test.TEAMNAMEREAL); await Test.prepareDeliverables(); @@ -117,12 +128,12 @@ describe("AdminController", () => { await dbc.writeTeam(t); } - it("Should be able to get the config name.", async function() { + it("Should be able to get the config name.", async function () { const res = await AdminController.getName(); expect(res).to.equal(ConfigCourses.classytest); }); - it("Should not be able to get a user that doesn't exist.", async function() { + it("Should not be able to get a user that doesn't exist.", async function () { const USERNAME = "UNKNOWNUSER" + new Date().getTime(); const res = await cc.handleUnknownUser(USERNAME); expect(res).to.equal(null); // nothing should be returned @@ -131,20 +142,20 @@ describe("AdminController", () => { expect(person).to.equal(null); // should not exist }); - it("Should be able to get a list of students.", async function() { + it("Should be able to get a list of students.", async function () { const res = await ac.getStudents(); expect(res).to.be.an('array'); expect(res.length).to.be.greaterThan(0); const s: StudentTransport = { - firstName: 'first_' + Test.USER1.id, - lastName: 'last_' + Test.USER1.id, - id: Test.USER1.id, - githubId: Test.USER1.github, - userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Test.USER1.github, + firstName: 'first_' + Test.USER1.id, + lastName: 'last_' + Test.USER1.id, + id: Test.USER1.id, + githubId: Test.USER1.github, + userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Test.USER1.github, studentNum: null, - labId: 'l1a' + labId: 'l1a' }; expect(res).to.deep.include(s); // make sure at least one student with the right format is in there @@ -157,10 +168,10 @@ describe("AdminController", () => { expect(actual.length).to.be.greaterThan(0); const t: TeamTransport = { - id: Test.TEAMNAME1, + id: Test.TEAMNAME1, delivId: "d0", - people: [Test.USER1.id, Test.USER2.id], - URL: null + people: [Test.USER1.id, Test.USER2.id], + URL: null // repoName: null, // repoUrl: null }; @@ -177,15 +188,15 @@ describe("AdminController", () => { const url = Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Test.USER2.github; const id = Test.USER2.id; const t: GradeTransport = { - personId: id, + personId: id, personURL: url, - delivId: "d1", - score: 100, - comment: "comment", - urlName: "urlName", - URL: "URL", + delivId: "d1", + score: 100, + comment: "comment", + urlName: "urlName", + URL: "URL", timestamp: 1517446860000, - custom: {} + custom: {} }; expect(res).to.deep.include(t); // make sure at least one student with the right format is in there }); @@ -272,16 +283,16 @@ describe("AdminController", () => { const grade: AutoTestGradeTransport = { delivId: 'd0', - score: 100, // grade: < 0 will mean 'N/A' in the UI + score: 100, // grade: < 0 will mean 'N/A' in the UI comment: '', // simple grades will just have a comment urlName: 'commitName', // description to go with the URL (repo if exists) - URL: 'commitUrl', // commit URL if known, otherwise repo URL (commit / repo if exists) + URL: 'commitUrl', // commit URL if known, otherwise repo URL (commit / repo if exists) timestamp: new Date(1400000000000 + 1000).getTime(), // shouldSave should be true - custom: {}, + custom: {}, - repoId: Test.REPONAME1, + repoId: Test.REPONAME1, repoURL: 'repoUrl' }; @@ -295,16 +306,16 @@ describe("AdminController", () => { const grade: AutoTestGradeTransport = { delivId: 'd0', - score: 100, // grade: < 0 will mean 'N/A' in the UI + score: 100, // grade: < 0 will mean 'N/A' in the UI comment: '', // simple grades will just have a comment urlName: 'commitName', // description to go with the URL (repo if exists) - URL: 'commitUrl', // commit URL if known, otherwise repo URL (commit / repo if exists) + URL: 'commitUrl', // commit URL if known, otherwise repo URL (commit / repo if exists) timestamp: new Date(1500000000000 + 1000).getTime(), // too late: shouldSave should be false - custom: {}, + custom: {}, - repoId: Test.REPONAME1, + repoId: Test.REPONAME1, repoURL: 'repoUrl' }; @@ -318,16 +329,16 @@ describe("AdminController", () => { const grade: AutoTestGradeTransport = { delivId: 'd0', - score: 100, // grade: < 0 will mean 'N/A' in the UI + score: 100, // grade: < 0 will mean 'N/A' in the UI comment: '', // simple grades will just have a comment urlName: 'commitName', // description to go with the URL (repo if exists) - URL: 'commitUrl', // commit URL if known, otherwise repo URL (commit / repo if exists) + URL: 'commitUrl', // commit URL if known, otherwise repo URL (commit / repo if exists) timestamp: Date.now(), // even if grade < 0 might as well return when the entry was made - custom: {}, + custom: {}, - repoId: 'INVALIDID', + repoId: 'INVALIDID', repoURL: 'repoUrl' }; @@ -368,7 +379,7 @@ describe("AdminController", () => { await ac.saveCourse(res); }); - it("Should not be able to validate an invalid course object.", function() { + it("Should not be able to validate an invalid course object.", function () { let res = null; try { AdminController.validateCourseTransport(null); @@ -389,7 +400,7 @@ describe("AdminController", () => { expect(res).to.be.an('string'); }); - it("Should not be able to validate an invalid provision object.", function() { + it("Should not be able to validate an invalid provision object.", function () { let res = null; let ex = null; try { @@ -461,7 +472,7 @@ describe("AdminController", () => { // await clearAndPreparePartial(); // }); - beforeEach(function() { + beforeEach(function () { const exec = Test.runSlowTest(); if (exec) { Log.test("AdminControllerSpec::slowTests - running: " + this.currentTest.title); diff --git a/packages/portal/backend/test/controllers/GitHubActionSpec.ts b/packages/portal/backend/test/controllers/GitHubActionSpec.ts index e9c12c7df..f665455f7 100644 --- a/packages/portal/backend/test/controllers/GitHubActionSpec.ts +++ b/packages/portal/backend/test/controllers/GitHubActionSpec.ts @@ -42,7 +42,7 @@ describe("GitHubActions", () => { await Test.prepareAll(); }); - beforeEach(function() { + beforeEach(function () { gh.setPageSize(2); // force a small page size for testing const exec = Test.runSlowTest(); @@ -55,7 +55,7 @@ describe("GitHubActions", () => { } }); - afterEach(function() { + afterEach(function () { gh.setPageSize(100); }); @@ -97,24 +97,24 @@ describe("GitHubActions", () => { Test.TEAMNAMEREAL ]; - it("Clear stale repos and teams.", async function() { + it("Clear stale repos and teams.", async function () { // this shouldn't be a test, but the before times out if we don't do it here const del = await deleteStale(); expect(del).to.be.true; }).timeout(TIMEOUT * 100); - it("Should not be possible to find a repo that does not exist.", async function() { + it("Should not be possible to find a repo that does not exist.", async function () { const val = await gh.repoExists(Test.INVALIDREPONAME); expect(val).to.be.false; }).timeout(TIMEOUT); - it("Should not be possible to delete a repo that does not exist.", async function() { + it("Should not be possible to delete a repo that does not exist.", async function () { // and it should do so without crashing const val = await gh.deleteRepo(Test.INVALIDREPONAME); expect(val).to.be.false; }).timeout(TIMEOUT); - it("Should be able to create a repo.", async function() { + it("Should be able to create a repo.", async function () { const rc = new RepositoryController(); const dc = new DeliverablesController(); const deliv = await dc.getDeliverable(Test.DELIVID0); @@ -126,7 +126,7 @@ describe("GitHubActions", () => { expect(val).to.equal(name); }).timeout(TIMEOUT); - it("Should fail to create a repo if there is no corresponding Repository object.", async function() { + it("Should fail to create a repo if there is no corresponding Repository object.", async function () { let res = null; let ex = null; try { @@ -138,29 +138,29 @@ describe("GitHubActions", () => { expect(ex).to.not.be.null; }).timeout(TIMEOUT); - it("Should be possible to find a repo that does exist.", async function() { + it("Should be possible to find a repo that does exist.", async function () { const val = await gh.repoExists(REPONAME); expect(val).to.be.true; }).timeout(TIMEOUT); - it("Should be able to remove a repo that does exist.", async function() { + it("Should be able to remove a repo that does exist.", async function () { const val = await gh.deleteRepo(REPONAME); expect(val).to.be.true; }).timeout(TIMEOUT); - it("Should be able to create the repo again.", async function() { + it("Should be able to create the repo again.", async function () { const val = await gh.createRepo(REPONAME); const name = Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Config.getInstance().getProp(ConfigKey.org) + '/' + REPONAME; expect(val).to.equal(name); }).timeout(TIMEOUT); - it("Should be able to list a webhook.", async function() { + it("Should be able to list a webhook.", async function () { const val = await gh.listWebhooks(REPONAME); expect(val).to.be.empty; }).timeout(TIMEOUT); - it("Should be able to create a webhook.", async function() { + it("Should be able to create a webhook.", async function () { let hooks = await gh.listWebhooks(REPONAME); // REPONAME expect(hooks).to.be.empty; @@ -173,7 +173,7 @@ describe("GitHubActions", () => { expect((hooks[0] as any).config.url).to.equal(hookName); }).timeout(TIMEOUT); - it("Should be able to edit a webhook.", async function() { + it("Should be able to edit a webhook.", async function () { let hooks = await gh.listWebhooks(REPONAME); expect(hooks).to.have.lengthOf(1); @@ -190,14 +190,14 @@ describe("GitHubActions", () => { expect(newHook).to.equal(NEWHOOK); }).timeout(TIMEOUT); - it("Should be possible to list the repos in an org.", async function() { + it("Should be possible to list the repos in an org.", async function () { const res = await gh.listRepos(); Log.test('# repos ' + res.length); expect(res).to.be.an('array'); expect(res.length).to.be.greaterThan(0); }).timeout(TIMEOUT); - it("Should be possible to list people in an org.", async function() { + it("Should be possible to list people in an org.", async function () { // gh.setPageSize(100); const res = await gh.listPeople(); Log.test('# people ' + res.length); @@ -205,14 +205,14 @@ describe("GitHubActions", () => { expect(res.length).to.be.greaterThan(0); }).timeout(TIMEOUT); - it("Should be possible to list the teams in an org.", async function() { + it("Should be possible to list the teams in an org.", async function () { const res = await gh.listTeams(); Log.test('# teams ' + res.length); expect(res).to.be.an('array'); expect(res.length).to.be.greaterThan(0); }).timeout(TIMEOUT); - it("Should be possible to identify an admin from the admin team.", async function() { + it("Should be possible to identify an admin from the admin team.", async function () { let res = await gh.isOnAdminTeam(Test.ADMIN1.github); Log.test('res: ' + res); expect(res).to.be.an('boolean'); @@ -232,7 +232,7 @@ describe("GitHubActions", () => { }).timeout(TIMEOUT); - it("Should be possible to identify a staff from the staff team.", async function() { + it("Should be possible to identify a staff from the staff team.", async function () { let res = await gh.isOnStaffTeam(Test.STAFF1.github); Log.test('res: ' + res); expect(res).to.be.an('boolean'); @@ -251,13 +251,13 @@ describe("GitHubActions", () => { expect(res).to.be.false; }).timeout(TIMEOUT); - it("Should not be possible to get a team number for a team that does not exist.", async function() { + it("Should not be possible to get a team number for a team that does not exist.", async function () { const val = await gh.getTeamNumber(Test.INVALIDTEAMNAME); Log.test('Team # ' + val); expect(val).to.be.lessThan(0); }).timeout(TIMEOUT); - it("Should be able to create a team, add users to it, and add it to the repo.", async function() { + it("Should be able to create a team, add users to it, and add it to the repo.", async function () { const val = await gh.createTeam(TEAMNAME, 'push'); Log.test("Team created; details: " + JSON.stringify(val)); expect(val.teamName).to.equal(TEAMNAME); @@ -281,7 +281,7 @@ describe("GitHubActions", () => { }).timeout(TIMEOUT); - it("Should be possible to get a team number for a team that does exist.", async function() { + it("Should be possible to get a team number for a team that does exist.", async function () { const val = await gh.getTeamNumber(Test.TEAMNAME1); Log.test('Team # ' + val); expect(val).to.be.greaterThan(0); @@ -290,7 +290,7 @@ describe("GitHubActions", () => { // expect(bool).to.be.true; }).timeout(TIMEOUT); - it("Should fail to get team members for an invalid team number argument.", async function() { + it("Should fail to get team members for an invalid team number argument.", async function () { let val = null; let ex = null; try { @@ -302,24 +302,26 @@ describe("GitHubActions", () => { expect(ex).to.not.be.null; }).timeout(TIMEOUT); - it("Should get an empty array of team members for a team that does not exist.", async function() { - const val = await gh.getTeamMembers(-1337); + it("Should get an empty array of team members for a team that does not exist.", async function () { + // const val = await gh.getTeamMembers(-1337); + const val = await gh.getTeamMembers("team_" + Date.now()); Log.test('# Team members: ' + val.length); expect(val.length).to.equal(0); }).timeout(TIMEOUT); - it("Should be able to get member names for a valid team.", async function() { - const teamnum = await gh.getTeamNumber('staff'); - Log.test("staff team #: " + teamnum); - expect(teamnum).to.be.an('number'); - expect(teamnum > 0).to.be.true; - const val = await gh.getTeamMembers(teamnum); + it("Should be able to get member names for a valid team.", async function () { + // const teamnum = await gh.getTeamNumber('staff'); + // Log.test("staff team #: " + teamnum); + // expect(teamnum).to.be.an('number'); + // expect(teamnum > 0).to.be.true; + // const val = await gh.getTeamMembers(teamnum); + const val = await gh.getTeamMembers('staff'); Log.test('# Team members: ' + val.length); expect(val.length).to.be.greaterThan(0); expect(val).to.contain(Test.ADMINSTAFF1.github); }).timeout(TIMEOUT); - it("Should be able to create many teams and get their numbers (tests team paging).", async function() { + it("Should be able to create many teams and get their numbers (tests team paging).", async function () { gh.setPageSize(2); // force a small page size for testing const NUM_TEAMS = 4; // could do 100 for a special test, but this is really slow @@ -351,7 +353,7 @@ describe("GitHubActions", () => { }).timeout(TIMEOUT * 20); - it("Should be able to create many repos and get them back (tests repo paging).", async function() { + it("Should be able to create many repos and get them back (tests repo paging).", async function () { const NUM_REPOS = 4; const rc = new RepositoryController(); @@ -391,7 +393,7 @@ describe("GitHubActions", () => { }).timeout(TIMEOUT * 1000); - it("Should be able to clone a source repo into a newly created repository.", async function() { + it("Should be able to clone a source repo into a newly created repository.", async function () { const start = Date.now(); const targetUrl = Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Config.getInstance().getProp(ConfigKey.org) + '/' + REPONAME; @@ -404,7 +406,7 @@ describe("GitHubActions", () => { Log.test('Full clone took: ' + Util.took(start)); }).timeout(120 * 1000); // 2 minutes - it("Should be able to clone a source repository, and select files to create a new repository.", async function() { + it("Should be able to clone a source repository, and select files to create a new repository.", async function () { const tc: TeamController = new TeamController(); const rc: RepositoryController = new RepositoryController(); const dc: DeliverablesController = new DeliverablesController(); @@ -445,7 +447,7 @@ describe("GitHubActions", () => { Log.test('Partial clone took: ' + Util.took(start)); }).timeout(120 * 1000); - it("Should be able to clone a source repository given various import URLs", async function() { + it("Should be able to clone a source repository given various import URLs", async function () { const githubHost = Config.getInstance().getProp(ConfigKey.githubHost); const targetUrl = Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Config.getInstance().getProp(ConfigKey.org) + '/' + REPONAME3; @@ -466,10 +468,10 @@ describe("GitHubActions", () => { [Test.REPONAMEREAL_TESTINGSAMPLE + ".git#" + Test.REPOBRANCHREAL_TESTINGSAMPLE, "FILE.txt"], // Should support importing from a subdir on a branch [Test.REPONAMEREAL_TESTINGSAMPLE + ".git#" + Test.REPOBRANCHREAL_TESTINGSAMPLE + ":" + - Test.REPOSUBDIRREAL_TESTINGSAMPLE, undefined], + Test.REPOSUBDIRREAL_TESTINGSAMPLE, undefined], // Should support importing from a subdir on a branch with a seedFile [Test.REPONAMEREAL_TESTINGSAMPLE + ".git#" + Test.REPOBRANCHREAL_TESTINGSAMPLE + ":" + - Test.REPOSUBDIRREAL_TESTINGSAMPLE, "BRANCH_NESTED.txt"], + Test.REPOSUBDIRREAL_TESTINGSAMPLE, "BRANCH_NESTED.txt"], ]; for (const importTest of importTests) { @@ -492,7 +494,7 @@ describe("GitHubActions", () => { } }).timeout(60 * 1000 * 10); - it("Should be able to soft-write a file to a repo, where the file doesn't exist.", async function() { + it("Should be able to soft-write a file to a repo, where the file doesn't exist.", async function () { const targetUrl = Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Config.getInstance().getProp(ConfigKey.org) + '/' + REPONAME3; @@ -500,7 +502,7 @@ describe("GitHubActions", () => { expect(success).to.be.true; }).timeout(2 * TIMEOUT); - it("Should be able to hard-write a file to a repo, where the file doesn't exist.", async function() { + it("Should be able to hard-write a file to a repo, where the file doesn't exist.", async function () { const targetUrl = Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Config.getInstance().getProp(ConfigKey.org) + '/' + REPONAME3; @@ -508,7 +510,7 @@ describe("GitHubActions", () => { expect(success).to.be.true; }).timeout(2 * TIMEOUT); - it("Should be able to hard-write a file to a repo, where the file does exist.", async function() { + it("Should be able to hard-write a file to a repo, where the file does exist.", async function () { const targetUrl = Config.getInstance().getProp(ConfigKey.githubHost) + '/' + Config.getInstance().getProp(ConfigKey.org) + '/' + REPONAME3; @@ -516,12 +518,12 @@ describe("GitHubActions", () => { expect(success).to.be.true; }).timeout(2 * TIMEOUT); - it("Should not be able to soft-write a file to a repo that doesn't exist.", async function() { + it("Should not be able to soft-write a file to a repo that doesn't exist.", async function () { const success = await gh.writeFileToRepo("invalidurl.com", "test_file2.txt", "hello world!"); expect(success).to.be.false; }).timeout(2 * TIMEOUT); - it("Should not be able to hard-write a file to a repo that doesn't exist.", async function() { + it("Should not be able to hard-write a file to a repo that doesn't exist.", async function () { const success = await gh.writeFileToRepo("invalidurl.com", "test_file2.txt", "hello world!", true); expect(success).to.be.false; }).timeout(2 * TIMEOUT); @@ -529,7 +531,7 @@ describe("GitHubActions", () => { /** * This test is terrible, but gets the coverage tools to stop complaining. */ - it("Should make sure that actions can actually fail.", async function() { + it("Should make sure that actions can actually fail.", async function () { if (1 > 0) { // terrible skip return; @@ -568,7 +570,7 @@ describe("GitHubActions", () => { } try { - await gh.deleteTeam(-1); + await gh.deleteTeam("team_" + Date.now()); } catch (err) { // expected } @@ -615,7 +617,7 @@ describe("GitHubActions", () => { it("Should be able to create a repo, " + "create a team, add users to it, add it to the repo, " + - "and change their permissions", async function() { + "and change their permissions", async function () { const githubTeam = await gh.createTeam(TEAMNAME, 'push'); expect(githubTeam.teamName).to.be.equal(TEAMNAME); expect(githubTeam.githubTeamNumber).to.be.an('number'); @@ -636,13 +638,13 @@ describe("GitHubActions", () => { }).timeout(TIMEOUT); - it("Should not be able to change permissions of a repo that does not exist.", async function() { + it("Should not be able to change permissions of a repo that does not exist.", async function () { const permissionEdit = await gh.setRepoPermission(Test.INVALIDREPONAME, "pull"); expect(permissionEdit).to.be.false; }).timeout(TIMEOUT); - it("Should not be able to change permissions of a repo to an invalid value.", async function() { + it("Should not be able to change permissions of a repo to an invalid value.", async function () { let permissionEdit = null; let ex = null; try { @@ -681,13 +683,13 @@ describe("GitHubActions", () => { // // }).timeout(TIMEOUT); - it("Should be possible to find the teams on a repo.", async function() { + it("Should be possible to find the teams on a repo.", async function () { const val = await gh.getTeamsOnRepo(Test.REPONAMEREAL); expect(val).to.be.an('array'); expect(val.length).to.equal(0); }).timeout(TIMEOUT); - it("Should be possible to get the team from a number.", async function() { + it("Should be possible to get the team from a number.", async function () { const teamNumber = await gh.getTeamNumber(TEAMNAME); expect(teamNumber).to.be.greaterThan(0); @@ -697,7 +699,7 @@ describe("GitHubActions", () => { expect(val.teamName).to.equal(TEAMNAME); }).timeout(TIMEOUT); - it("Should not be possible to get the team that does not exist.", async function() { + it("Should not be possible to get the team that does not exist.", async function () { let res: any = "exists"; let ex = null; try { @@ -709,7 +711,7 @@ describe("GitHubActions", () => { expect(ex).to.be.null; }).timeout(TIMEOUT); - it("Should not be possible to get the team with an invalid param.", async function() { + it("Should not be possible to get the team with an invalid param.", async function () { let res = null; let ex = null; try { @@ -721,7 +723,7 @@ describe("GitHubActions", () => { expect(ex).to.not.be.null; }).timeout(TIMEOUT); - it("Should be possible to check the database.", async function() { + it("Should be possible to check the database.", async function () { let res = await GitHubActions.checkDatabase(null, null); expect(res).to.be.true; @@ -730,7 +732,7 @@ describe("GitHubActions", () => { expect(res).to.be.true; }).timeout(TIMEOUT); - it("Should not be possible to simulate a webhook with the wrong params.", async function() { + it("Should not be possible to simulate a webhook with the wrong params.", async function () { let worked = await gh.simulateWebookComment(null, "SHA", "message"); expect(worked).to.be.false; @@ -741,7 +743,7 @@ describe("GitHubActions", () => { expect(worked).to.be.false; }).timeout(TIMEOUT); - it("Should be possible to simulate a webhook.", async function() { + it("Should be possible to simulate a webhook.", async function () { let worked = await gh.simulateWebookComment(Test.REPONAMEREAL_POSTTEST, "SHA", "message"); expect(worked).to.be.false; // SHA is not right @@ -765,7 +767,7 @@ describe("GitHubActions", () => { expect(ex).to.be.null; // at least don't throw an exception }).timeout(TIMEOUT); - it("Should not be possible to make a comment with invalid params.", async function() { + it("Should not be possible to make a comment with invalid params.", async function () { let worked = await gh.makeComment(null, "message"); expect(worked).to.be.false; @@ -773,7 +775,7 @@ describe("GitHubActions", () => { expect(worked).to.be.false; }).timeout(TIMEOUT); - it("Should be possible to make a comment.", async function() { + it("Should be possible to make a comment.", async function () { const githubAPI = Config.getInstance().getProp(ConfigKey.githubAPI); let msg = "message"; let url = githubAPI + '/repos/classytest/' + Test.REPONAMEREAL_POSTTEST + '/commits/INVALIDSHA/comments'; @@ -791,7 +793,7 @@ describe("GitHubActions", () => { expect(worked).to.be.true; // should have worked }).timeout(TIMEOUT); - it("Should be possible to find the teams on a repo.", async function() { + it("Should be possible to find the teams on a repo.", async function () { const val = await gh.getTeamsOnRepo(REPONAME); Log.test("listed teams: " + JSON.stringify(val)); expect(val).to.be.an('array'); @@ -815,7 +817,7 @@ describe("GitHubActions", () => { }).timeout(TIMEOUT); - it("Should be possible to find the members of a team.", async function() { + it("Should be possible to find the members of a team.", async function () { const val = await gh.listTeamMembers(TEAMNAME); Log.test("listed members: " + JSON.stringify(val)); expect(val).to.be.an('array'); @@ -824,7 +826,7 @@ describe("GitHubActions", () => { expect(val).to.include(Test.GITHUB2.github); }).timeout(TIMEOUT); - it("Clear stale repos and teams.", async function() { + it("Clear stale repos and teams.", async function () { const del = await deleteStale(); expect(del).to.be.true; }).timeout(TIMEOUT * 10); @@ -883,7 +885,8 @@ describe("GitHubActions", () => { for (const t of TESTTEAMNAMES) { if (team.teamName === t) { Log.test("Removing stale team: " + team.teamName); - await gh.deleteTeam(team.teamNumber); + // await gh.deleteTeam(team.teamNumber); + await gh.deleteTeam(team.teamName); await Util.delay(DELAY_SHORT); done = true; } @@ -891,7 +894,8 @@ describe("GitHubActions", () => { if (done === false) { if (team.teamName.startsWith(TEAMNAME) === true) { Log.test("Removing stale team: " + team.teamName); - await gh.deleteTeam(team.teamNumber); + // await gh.deleteTeam(team.teamNumber); + await gh.deleteTeam(team.teamName); await Util.delay(DELAY_SHORT); } } diff --git a/packages/portal/backend/test/server/AdminRoutesSpec.ts b/packages/portal/backend/test/server/AdminRoutesSpec.ts index 6cd346a81..8a2b804a2 100644 --- a/packages/portal/backend/test/server/AdminRoutesSpec.ts +++ b/packages/portal/backend/test/server/AdminRoutesSpec.ts @@ -29,7 +29,7 @@ import {TeamController} from "../../src/controllers/TeamController"; import BackendServer from "../../src/server/BackendServer"; import './AuthRoutesSpec'; -describe('Admin Routes', function() { +describe('Admin Routes', function () { let app: restify.Server = null; let server: BackendServer = null; @@ -71,7 +71,7 @@ describe('Admin Routes', function() { await Test.suiteAfter('Admin Routes'); }); - it('Should be able to get a list of students', async function() { + it('Should be able to get a list of students', async function () { let response = null; let body: StudentTransportPayload; @@ -89,7 +89,7 @@ describe('Admin Routes', function() { // should confirm body.success objects (at least one) }).timeout(Test.TIMEOUT); - it('Should be able to get a list of students with cookies for authentication', async function() { + it('Should be able to get a list of students with cookies for authentication', async function () { let response = null; let body: StudentTransportPayload; @@ -107,7 +107,7 @@ describe('Admin Routes', function() { // should confirm body.success objects (at least one) }).timeout(Test.TIMEOUT); - it('Should not be able to get a list of students if the requestor is not privileged', async function() { + it('Should not be able to get a list of students if the requestor is not privileged', async function () { let response = null; let body: StudentTransportPayload; @@ -124,7 +124,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should not be able to get a list of students with bad cookies for auth', async function() { + it('Should not be able to get a list of students with bad cookies for auth', async function () { let response = null; let body: StudentTransportPayload; @@ -141,7 +141,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should not be able to get a list of students without any auth data', async function() { + it('Should not be able to get a list of students without any auth data', async function () { let response = null; let body: StudentTransportPayload; @@ -158,7 +158,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should be able to get a list of teams', async function() { + it('Should be able to get a list of teams', async function () { let response = null; let body: TeamTransportPayload; @@ -177,7 +177,7 @@ describe('Admin Routes', function() { // should confirm body.success objects (at least one) }); - it('Should not be able to get a list of teams if the requestor is not privileged', async function() { + it('Should not be able to get a list of teams if the requestor is not privileged', async function () { let response = null; let body: StudentTransportPayload; @@ -194,7 +194,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should be able to get a list of students', async function() { + it('Should be able to get a list of students', async function () { let response = null; let body: StudentTransportPayload; @@ -213,7 +213,7 @@ describe('Admin Routes', function() { // should confirm body.success objects (at least one) }).timeout(Test.TIMEOUT); - it('Should not be able to get a list of grades if the requestor is not privileged', async function() { + it('Should not be able to get a list of grades if the requestor is not privileged', async function () { let response = null; let body: StudentTransportPayload; @@ -230,7 +230,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should be able to get a list of results', async function() { + it('Should be able to get a list of results', async function () { let response = null; let body: AutoTestResultPayload; @@ -250,7 +250,7 @@ describe('Admin Routes', function() { // should confirm body.success objects (at least one) }); - it('Should not be able to get a list of results if the requestor is not privileged', async function() { + it('Should not be able to get a list of results if the requestor is not privileged', async function () { let response = null; let body: AutoTestResultPayload; @@ -267,7 +267,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should be able to get a list of dashboard results', async function() { + it('Should be able to get a list of dashboard results', async function () { let response = null; let body: AutoTestResultPayload; @@ -287,7 +287,7 @@ describe('Admin Routes', function() { // should confirm body.success objects (at least one) }); - it('Should not be able to get a list of dashboard results if the requestor is not privileged', async function() { + it('Should not be able to get a list of dashboard results if the requestor is not privileged', async function () { let response = null; let body: AutoTestResultPayload; @@ -304,7 +304,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should be able to export the list of dashboard results', async function() { + it('Should be able to export the list of dashboard results', async function () { let response = null; let body: AutoTestResultPayload; @@ -324,7 +324,7 @@ describe('Admin Routes', function() { // should confirm body.success objects (at least one) }); - it('Should not be able to export the list of dashboard results if the requestor is not privileged', async function() { + it('Should not be able to export the list of dashboard results if the requestor is not privileged', async function () { let response = null; let body: AutoTestResultPayload; @@ -341,7 +341,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should be able to get a list of repositories', async function() { + it('Should be able to get a list of repositories', async function () { let response = null; let body: RepositoryPayload; @@ -364,7 +364,7 @@ describe('Admin Routes', function() { expect(entry.URL).to.not.be.undefined; }); - it('Should be able to get a list of deliverables', async function() { + it('Should be able to get a list of deliverables', async function () { let response = null; let body: DeliverableTransportPayload; @@ -386,7 +386,7 @@ describe('Admin Routes', function() { expect(actual).to.be.null; // make sure at least one of the deliverables validates }); - it('Should be able to create a new deliverable', async function() { + it('Should be able to create a new deliverable', async function () { let response = null; let body: Payload; @@ -405,7 +405,7 @@ describe('Admin Routes', function() { expect(body.success.message).to.be.an('string'); }); - it('Should fail to create a new deliverable if the object is invalid', async function() { + it('Should fail to create a new deliverable if the object is invalid', async function () { let response = null; let body: Payload; @@ -426,7 +426,7 @@ describe('Admin Routes', function() { expect(body.failure.message).to.be.an('string'); }); - it('Should fail to create a new deliverable if the user is not an admin', async function() { + it('Should fail to create a new deliverable if the user is not an admin', async function () { // this test looks like overkill // but we want to have @@ -457,7 +457,7 @@ describe('Admin Routes', function() { expect(body.failure.message).to.be.an('string'); }); - it('Should be able to update a deliverable', async function() { + it('Should be able to update a deliverable', async function () { let response = null; let body: Payload; @@ -472,27 +472,27 @@ describe('Admin Routes', function() { at.closeTimestamp = d0.closeTimestamp; const deliv: DeliverableTransport = { - id: d0.id, - openTimestamp: d0.openTimestamp, - closeTimestamp: d0.closeTimestamp, - shouldProvision: d0.shouldProvision, - importURL: d0.importURL, - minTeamSize: d0.teamMinSize, - maxTeamSize: d0.teamMaxSize, - teamsSameLab: d0.teamSameLab, + id: d0.id, + openTimestamp: d0.openTimestamp, + closeTimestamp: d0.closeTimestamp, + shouldProvision: d0.shouldProvision, + importURL: d0.importURL, + minTeamSize: d0.teamMinSize, + maxTeamSize: d0.teamMaxSize, + teamsSameLab: d0.teamSameLab, studentsFormTeams: d0.teamStudentsForm, - onOpenAction: '', - onCloseAction: '', - repoPrefix: d0.repoPrefix, - teamPrefix: d0.teamPrefix, + onOpenAction: '', + onCloseAction: '', + repoPrefix: d0.repoPrefix, + teamPrefix: d0.teamPrefix, visibleToStudents: d0.visibleToStudents, - URL: d0.URL, - gradesReleased: d0.gradesReleased, - lateAutoTest: d0.lateAutoTest, - shouldAutoTest: d0.shouldAutoTest, - autoTest: at, - rubric: d0.rubric, - custom: d0.custom + URL: d0.URL, + gradesReleased: d0.gradesReleased, + lateAutoTest: d0.lateAutoTest, + shouldAutoTest: d0.shouldAutoTest, + autoTest: at, + rubric: d0.rubric, + custom: d0.custom }; // make sure the times were not already the new time @@ -525,14 +525,14 @@ describe('Admin Routes', function() { Log.test('update did update the value'); }); - it('Should be able to upload a new classlist', async function() { + it('Should be able to upload a new classlist', async function () { let response = null; let body: Payload; const url = '/portal/admin/classlist'; try { response = await request(app).post(url).attach('classlist', __dirname + '/../data/classlistValidFirst.csv').set({ - user: userName, + user: userName, token: userToken }); body = response.body; @@ -547,14 +547,14 @@ describe('Admin Routes', function() { expect(body.success.classlist.length).to.equal(5); }); - it('Should fail to upload bad classlists', async function() { + it('Should fail to upload bad classlists', async function () { let response = null; let body: Payload; const url = '/portal/admin/classlist'; response = await request(app).post(url).attach('classlist', __dirname + '/../data/classlistInvalid.csv').set({ - user: userName, + user: userName, token: userToken }); body = response.body; @@ -564,7 +564,7 @@ describe('Admin Routes', function() { expect(body.failure.message).to.be.an('string'); // test column missing response = await request(app).post(url).attach('classlist', __dirname + '/../data/classlistEmpty.csv').set({ - user: userName, + user: userName, token: userToken }); body = response.body; @@ -575,7 +575,7 @@ describe('Admin Routes', function() { expect(body.failure.message).to.contain('no students'); }); - it('Should be able to upload an updated classlist', async function() { + it('Should be able to upload an updated classlist', async function () { const dc = DatabaseController.getInstance(); let people = await dc.getPeople(); @@ -589,7 +589,7 @@ describe('Admin Routes', function() { const url = '/portal/admin/classlist'; try { response = await request(app).post(url).attach('classlist', __dirname + '/../data/classlistValidUpdate.csv').set({ - user: userName, + user: userName, token: userToken }); body = response.body; @@ -611,14 +611,14 @@ describe('Admin Routes', function() { expect(person.studentNumber).to.equal(newPerson.studentNumber); // should be the same }); - it('Should be able to upload a new grades', async function() { + it('Should be able to upload a new grades', async function () { let response = null; let body: Payload; const url = '/portal/admin/grades/' + Test.DELIVID1; try { response = await request(app).post(url).attach('gradelist', __dirname + '/../data/gradesValid.csv').set({ - user: userName, + user: userName, token: userToken }); body = response.body; @@ -633,14 +633,14 @@ describe('Admin Routes', function() { expect(body.success.message).to.contain('3 grades'); }); - it('Should fail to upload a bad grades list', async function() { + it('Should fail to upload a bad grades list', async function () { let response = null; let body: Payload; const url = '/portal/admin/grades/' + Test.DELIVID1; response = await request(app).post(url).attach('gradelist', __dirname + '/../data/gradesInvalid.csv').set({ - user: userName, + user: userName, token: userToken }); body = response.body; @@ -651,7 +651,7 @@ describe('Admin Routes', function() { expect(body.failure.message).to.contain('column missing'); response = await request(app).post(url).attach('gradelist', __dirname + '/../data/gradesEmpty.csv').set({ - user: userName, + user: userName, token: userToken }); body = response.body; @@ -662,7 +662,7 @@ describe('Admin Routes', function() { expect(body.failure.message).to.contain('no grades'); }); - it('Should be able to get the course object', async function() { + it('Should be able to get the course object', async function () { let response = null; let body: CourseTransportPayload; @@ -681,7 +681,7 @@ describe('Admin Routes', function() { // TODO: check response properties }); - it('Should be able to update the course object', async function() { + it('Should be able to update the course object', async function () { let response = null; let body: Payload; @@ -690,9 +690,9 @@ describe('Admin Routes', function() { const newId = Date.now() + 'id'; const course: CourseTransport = { - id: Config.getInstance().getProp(ConfigKey.testname), + id: Config.getInstance().getProp(ConfigKey.testname), defaultDeliverableId: newId, - custom: {} + custom: {} }; response = await request(app).post(url).send(course).set({user: userName, token: userToken}); body = response.body; @@ -709,7 +709,7 @@ describe('Admin Routes', function() { expect(response.status).to.equal(200); }); - it('Should not be able to update the course object with invalid settings', async function() { + it('Should not be able to update the course object with invalid settings', async function () { let response = null; let body: Payload; @@ -720,7 +720,7 @@ describe('Admin Routes', function() { const course: any = { // id: 'some id', // THIS IS A REQUIRED FIELD defaultDeliverableId: newId, - custom: {} + custom: {} }; response = await request(app).post(url).send(course).set({user: userName, token: userToken}); body = response.body; @@ -733,7 +733,7 @@ describe('Admin Routes', function() { describe("Slow AdminRoute Tests", () => { - beforeEach(function() { + beforeEach(function () { const exec = Test.runSlowTest(); if (exec) { @@ -768,17 +768,19 @@ describe('Admin Routes', function() { } for (const teamName of teamNames) { - const cacheNum = await tcCache.getTeamNumber(teamName); // ghCache.getTeamNumber(teamName); - await ghCache.deleteTeam(cacheNum); + // const cacheNum = await tcCache.getTeamNumber(teamName); // ghCache.getTeamNumber(teamName); + // await ghCache.deleteTeam(cacheNum); + await ghCache.deleteTeam(teamName); - const realNum = await tcReal.getTeamNumber(teamName); // ghCache.getTeamNumber(teamName); - await ghReal.deleteTeam(realNum); + // const realNum = await tcReal.getTeamNumber(teamName); // ghCache.getTeamNumber(teamName); + // await ghReal.deleteTeam(realNum); + await ghReal.deleteTeam(teamName); } Log.test("AdminRoutesSpec::clearAll() - done; took: " + Util.took(start)); } - it('Should be able to get a provision plan for a deliverable', async function() { + it('Should be able to get a provision plan for a deliverable', async function () { let response = null; let body: RepositoryPayload; @@ -818,7 +820,7 @@ describe('Admin Routes', function() { // expect(body.success[0].id).to.equal(Test.REPONAME1); // }).timeout(TIMEOUT * 30); - it('Should be able to get a release plan for a deliverable', async function() { + it('Should be able to get a release plan for a deliverable', async function () { let response = null; let body: RepositoryPayload; @@ -858,7 +860,7 @@ describe('Admin Routes', function() { // expect(body.success.length).to.equal(0); // NOTE: this is terrible, something should be being released // }).timeout(TIMEOUT * 30); - it('Should be able to perform a withdraw task', async function() { + it('Should be able to perform a withdraw task', async function () { // This is tricky because the live github data will have a different team id than we're using locally const pc = new PersonController(); @@ -879,7 +881,7 @@ describe('Admin Routes', function() { expect(body.success.message).to.be.an('string'); }).timeout(TIMEOUT * 10); - it('Should be able to sanity check a database', async function() { + it('Should be able to sanity check a database', async function () { let response = null; let body: Payload; @@ -896,7 +898,7 @@ describe('Admin Routes', function() { expect(body.success.message).to.be.an('string'); }).timeout(TIMEOUT * 10); - it('Should be able to provision a deliverable', async function() { + it('Should be able to provision a deliverable', async function () { const dbc = DatabaseController.getInstance(); await dbc.clearData(); @@ -949,7 +951,7 @@ describe('Admin Routes', function() { }).timeout(Test.TIMEOUTLONG); - it('Should fail to provision a deliverable if invalid options are given', async function() { + it('Should fail to provision a deliverable if invalid options are given', async function () { let response = null; let body: Payload; @@ -986,7 +988,7 @@ describe('Admin Routes', function() { expect(body.failure).to.not.be.undefined; }); - it('Should be able to release a deliverable', async function() { + it('Should be able to release a deliverable', async function () { let response = null; let body: Payload; @@ -1013,7 +1015,7 @@ describe('Admin Routes', function() { // expect(body.success.length).to.equal(0); }).timeout(Test.TIMEOUTLONG); - it('Should fail to release a deliverable if invalid options are given', async function() { + it('Should fail to release a deliverable if invalid options are given', async function () { let response = null; let body: Payload; @@ -1045,7 +1047,7 @@ describe('Admin Routes', function() { }); }); - it('Should be able to create a team for a deliverable.', async function() { + it('Should be able to create a team for a deliverable.', async function () { let response = null; let body: Payload; @@ -1054,7 +1056,7 @@ describe('Admin Routes', function() { try { // create 2 people in an individual deliverable (should be allowed for admin) const team: TeamFormationTransport = { - delivId: Test.DELIVID0, + delivId: Test.DELIVID0, githubIds: [Test.USER5.github, Test.USER6.github] }; @@ -1072,14 +1074,14 @@ describe('Admin Routes', function() { expect(body.success.length).to.equal(1); }); - it('Should fail to create a team for a deliverable if something is invalid', async function() { + it('Should fail to create a team for a deliverable if something is invalid', async function () { let response = null; let body: Payload; const url = '/portal/admin/team'; let ex = null; const team: TeamFormationTransport = { - delivId: Test.DELIVID0, + delivId: Test.DELIVID0, githubIds: [Test.USER5.github, Test.USER6.github] }; try { @@ -1129,7 +1131,7 @@ describe('Admin Routes', function() { }).timeout(Test.TIMEOUT); - it('Should be able to delete a deliverable', async function() { + it('Should be able to delete a deliverable', async function () { const url = '/portal/admin/deliverable/' + Test.DELIVID0; let response = null; let body: Payload; @@ -1148,7 +1150,7 @@ describe('Admin Routes', function() { expect(ex).to.be.null; }); - it('Should fail to delete a deliverable if appropriate', async function() { + it('Should fail to delete a deliverable if appropriate', async function () { const url = '/portal/admin/deliverable/'; let response = null; let body: Payload; @@ -1185,7 +1187,7 @@ describe('Admin Routes', function() { expect(ex).to.be.null; }); - it('Should be able to delete a repository', async function() { + it('Should be able to delete a repository', async function () { const url = '/portal/admin/repository/' + Test.REPONAME1; let response = null; let body: Payload; @@ -1204,7 +1206,7 @@ describe('Admin Routes', function() { expect(ex).to.be.null; }).timeout(Test.TIMEOUT); - it('Should fail to delete a repository if appropriate', async function() { + it('Should fail to delete a repository if appropriate', async function () { const url = '/portal/admin/repository/'; let response = null; let body: Payload; @@ -1241,7 +1243,7 @@ describe('Admin Routes', function() { expect(ex).to.be.null; }); - it('Should be able to delete a team', async function() { + it('Should be able to delete a team', async function () { const url = '/portal/admin/team/' + Test.TEAMNAME1; let response = null; let body: Payload; @@ -1260,7 +1262,7 @@ describe('Admin Routes', function() { expect(ex).to.be.null; }); - it('Should fail to delete a team if appropriate', async function() { + it('Should fail to delete a team if appropriate', async function () { const url = '/portal/admin/team/'; let response = null; let body: Payload; @@ -1297,7 +1299,7 @@ describe('Admin Routes', function() { expect(ex).to.be.null; }); - it('Should be able to update a classlist if authorized as admin', async function() { + it('Should be able to update a classlist if authorized as admin', async function () { let response = null; let body: Payload; const url = '/portal/admin/classlist'; @@ -1314,7 +1316,7 @@ describe('Admin Routes', function() { expect(body.success).to.haveOwnProperty('removed'); }); - it('Should NOT be able to update a classlist if not authorized as admin', async function() { + it('Should NOT be able to update a classlist if not authorized as admin', async function () { let response = null; let body: Payload; const url = '/portal/admin/classlist'; From 90038bf2a8083ee55ff8dcd4d06b934a54814579 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sun, 18 Sep 2022 22:23:37 -0700 Subject: [PATCH 005/104] More GitHub API type improvements (decent chance this will fail in CI) --- .../backend/src/controllers/GitHubActions.ts | 152 +++++++++--------- .../src/controllers/GitHubController.ts | 51 ++++-- 2 files changed, 114 insertions(+), 89 deletions(-) diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index d5a2250f9..69bbed438 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -8,7 +8,7 @@ import Util from "../../../../common/Util"; import {Factory} from "../Factory"; import {DatabaseController} from "./DatabaseController"; -import {BranchRule, GitTeamTuple, Issue} from "./GitHubController"; +import {BranchRule, GitPersonTuple, GitRepoTuple, GitTeamTuple, Issue} from "./GitHubController"; import {TeamController} from "./TeamController"; // tslint:disable-next-line @@ -73,7 +73,7 @@ export interface IGitHubActions { * This is just a subset of the return, but it is the subset we actually use: * @returns {Promise<{ id: number, name: string, url: string }[]>} */ - listRepos(): Promise>; + listRepos(): Promise; /** * Gets all people in an org. @@ -81,19 +81,19 @@ export interface IGitHubActions { * @returns {Promise<{ id: number, type: string, url: string, name: string }[]>} * this is just a subset of the return, but it is the subset we actually use */ - listPeople(): Promise>; + listPeople(): Promise; /** * Lists the teams for the current org. * * NOTE: this is a slow operation (if there are many teams) so try not to do it too much! * - * @returns {Promise<{id: number, name: string}[]>} + * @returns {Promise<{GitTeamTuple[]>} */ - listTeams(): Promise>; + listTeams(): Promise; /** - * Lists the Github IDs of members for a teamName (e.g. students). + * Lists the GitHub IDs of members for a teamName (e.g. students). * * @param {string} teamName * @returns {Promise} // list of githubIds @@ -109,26 +109,23 @@ export interface IGitHubActions { /** * Creates a team for a groupName (e.g., cpsc310_team1). * - * Returns the teamId (used by many other Github calls). - * * @param teamName * @param permission 'admin', 'pull', 'push' // admin for staff, push for students - * @returns {Promise} team id + * @returns {Promise} */ - createTeam(teamName: string, permission: string): Promise<{ teamName: string, githubTeamNumber: number, URL: string }>; + createTeam(teamName: string, permission: string): Promise; /** - * Add a list of Github members (their usernames) to a given team. + * Add a list of GitHub members (their usernames) to a given team. * * @param teamName - * @param githubTeamId * @param memberGithubIds: string[] // github usernames * @returns {Promise} */ addMembersToTeam(teamName: string, memberGithubIds: string[]): Promise; /** - * Removes a list of Github members (their usernames) from a given team. + * Removes a list of GitHub members (their usernames) from a given team. * * @param teamName * @param memberGithubIds: string[] // github usernames @@ -137,7 +134,7 @@ export interface IGitHubActions { removeMembersFromTeam(teamName: string, memberGithubIds: string[]): Promise; /** - * NOTE: needs the team Id (number), not the team name (string)! + * NOTE: needs the team ID (number), not the team name (string)! * * @param teamId * @param repoName @@ -446,7 +443,7 @@ export class GitHubActions implements IGitHubActions { * * NOTE: if you're deleting the 'admin', 'staff', or 'students' teams, you're doing something terribly wrong. * - * @param teamId + * @param teamName: string */ public async deleteTeam(teamName: string): Promise { @@ -500,7 +497,7 @@ export class GitHubActions implements IGitHubActions { * This is just a subset of the return, but it is the subset we actually use: * @returns {Promise<{ id: number, name: string, url: string }[]>} */ - public async listRepos(): Promise> { + public async listRepos(): Promise { Log.info("GitHubActions::listRepos(..) - start"); const start = Date.now(); @@ -518,12 +515,13 @@ export class GitHubActions implements IGitHubActions { const raw: any = await this.handlePagination(uri, options); - const rows: Array<{ repoName: string, repoNumber: number, url: string }> = []; + // const rows: Array<{ repoName: string, repoNumber: number, url: string }> = []; + const rows: GitRepoTuple[] = []; // Array<{ repoName: string, repoNumber: number, url: string }> = []; for (const entry of raw) { const id = entry.id; const name = entry.name; const url = entry.html_url; - rows.push({repoName: name, repoNumber: id, url: url}); + rows.push({repoName: name, githubRepoNumber: id, url: url}); } Log.info("GitHubActions::listRepos(..) - done; # repos: " + rows.length + "; took: " + Util.took(start)); @@ -537,7 +535,7 @@ export class GitHubActions implements IGitHubActions { * @returns {Promise<{ id: number, type: string, url: string, name: string }[]>} * this is just a subset of the return, but it is the subset we actually use */ - public async listPeople(): Promise> { + public async listPeople(): Promise { Log.info("GitHubActions::listPeople(..) - start"); const start = Date.now(); @@ -554,12 +552,12 @@ export class GitHubActions implements IGitHubActions { const raw: any = await this.handlePagination(uri, options); - const rows: Array<{ githubId: string, personNumber: number, url: string }> = []; + const rows: GitPersonTuple[] = []; for (const entry of raw) { const id = entry.id; const url = entry.html_url; const githubId = entry.login; - rows.push({githubId: githubId, personNumber: id, url: url}); + rows.push({githubId: githubId, githubPersonNumber: id, url: url}); } Log.info("GitHubActions::listPeople(..) - done; # people: " + rows.length + "; took: " + Util.took(start)); @@ -649,7 +647,8 @@ export class GitHubActions implements IGitHubActions { * * @returns {Promise<{id: number, name: string}[]>} */ - public async listTeams(): Promise> { + // public async listTeams(): Promise> { + public async listTeams(): Promise { // Log.info("GitHubActions::listTeams(..) - start"); const start = Date.now(); @@ -668,11 +667,11 @@ export class GitHubActions implements IGitHubActions { const teamsRaw: any = await this.handlePagination(uri, options); - const teams: Array<{ teamName: string, teamNumber: number }> = []; + const teams: GitTeamTuple[] = []; for (const team of teamsRaw) { const teamNumber = team.id; const teamName = team.name; - teams.push({teamNumber: teamNumber, teamName: teamName}); + teams.push({githubTeamNumber: teamNumber, teamName: teamName}); } Log.info("GitHubActions::listTeams(..) - done; # teams: " + teams.length + "; took: " + Util.took(start)); @@ -782,11 +781,11 @@ export class GitHubActions implements IGitHubActions { /** * Creates a team for a groupName (e.g., cpsc310_team1). * - * Returns the teamId (used by many other Github calls). + * Returns a team tuple. * * @param teamName * @param permission 'admin', 'pull', 'push' // admin for staff, push for students - * @returns {Promise} team id + * @returns {Promise} team tuple */ public async createTeam(teamName: string, permission: string): Promise<{ teamName: string, githubTeamNumber: number, URL: string }> { @@ -838,10 +837,9 @@ export class GitHubActions implements IGitHubActions { } /** - * Add a set of Github members (their usernames) to a given team. + * Add a set of GitHub members (their usernames) to a given team. * * @param teamName - * @param githubTeamId * @param members: string[] // github usernames * @returns {Promise} */ @@ -889,11 +887,10 @@ export class GitHubActions implements IGitHubActions { } /** - * Remove a set of Github members (their usernames) from a given team. + * Remove a set of GitHub members (their usernames) from a given team. * * @param teamName - * @param githubTeamId - * @param members: string[] // github usernames + * @param members: string[] // GitHub usernames to remove from the team * @returns {Promise} */ public async removeMembersFromTeam(teamName: string, members: string[]): Promise { @@ -940,7 +937,7 @@ export class GitHubActions implements IGitHubActions { } /** - * NOTE: needs the team Id (number), not the team name (string)! + * NOTE: needs the team teamId (number), not the team name (string)! * * @param teamId * @param repoName @@ -999,7 +996,7 @@ export class GitHubActions implements IGitHubActions { const teamList = await this.listTeams(); for (const team of teamList) { if (team.teamName === teamName) { - teamId = team.teamNumber; + teamId = team.githubTeamNumber; } } @@ -1937,8 +1934,12 @@ export class GitHubActions implements IGitHubActions { // tslint:disable-next-line export class TestGitHubActions implements IGitHubActions { + private teams: Map = new Map(); + public constructor() { Log.info("TestGitHubActions:: - start"); + this.teams.set(TeamController.STAFF_NAME, {teamName: TeamController.STAFF_NAME, githubTeamNumber: 1000}); + this.teams.set(TeamController.ADMIN_NAME, {teamName: TeamController.ADMIN_NAME, githubTeamNumber: 1001}); } public async addMembersToTeam(teamName: string, members: string[]): Promise { @@ -1980,16 +1981,19 @@ export class TestGitHubActions implements IGitHubActions { return this.repos[repoId]; } - public async createTeam(teamName: string, permission: string): Promise<{ teamName: string; githubTeamNumber: number; URL: string }> { - if (typeof this.teams[teamName] === 'undefined') { + // public async createTeam(teamName: string, permission: string): Promise<{ teamName: string; githubTeamNumber: number; URL: string }> { + public async createTeam(teamName: string, permission: string): Promise { + // if (typeof this.teams[teamName] === 'undefined') { + if (this.teams.has(teamName) === false) { const c = Config.getInstance(); const url = c.getProp(ConfigKey.githubHost) + '/' + c.getProp(ConfigKey.org) + '/teams/' + teamName; - this.teams[teamName] = {teamName: teamName, githubTeamNumber: Date.now(), URL: 'teamURL'}; + // this.teams[teamName] = {teamName: teamName, githubTeamNumber: Date.now(), URL: url}; + this.teams.set(teamName, {teamName: teamName, githubTeamNumber: Date.now()}); } Log.info("TestGitHubActions::teamCreate( " + teamName + " ) - created; exists: " + - (typeof this.teams[teamName] !== 'undefined') + "; records: " + JSON.stringify(this.teams)); + (this.teams.has(teamName)) + "; records: " + JSON.stringify(this.teams)); - return this.teams[teamName]; + return this.teams.get(teamName); } public async deleteRepo(repoName: string): Promise { @@ -2016,7 +2020,8 @@ export class TestGitHubActions implements IGitHubActions { public async deleteTeamByName(teamName: string): Promise { for (const name of Object.keys(this.teams)) { if (name === teamName) { - delete this.teams[teamName]; + // delete this.teams[teamName]; + this.teams.delete(teamName); return true; } } @@ -2026,11 +2031,12 @@ export class TestGitHubActions implements IGitHubActions { public async deleteTeam(teamNameToDelete: string): Promise { Log.info("TestGitHubActions::deleteTeam( " + teamNameToDelete + " )"); for (const teamName of Object.keys(this.teams)) { - const team = this.teams[teamName]; - // if (team.githubTeamNumber === teamId) { - if (team.name === teamNameToDelete) { - Log.info("TestGitHubActions::deleteTeam( " + teamNameToDelete + " ) - deleting team name: " + team.name); - delete this.teams[teamName]; + // const team = this.teams[teamName]; + const team = this.teams.get(teamName); + if (team.teamName === teamNameToDelete) { + Log.info("TestGitHubActions::deleteTeam( " + teamNameToDelete + " ) - deleting team name: " + team.teamName); + // delete this.teams[teamName]; + this.teams.delete(teamName); return true; } } @@ -2053,8 +2059,9 @@ export class TestGitHubActions implements IGitHubActions { // this is the team number for the students team in the classytest org on github.com return 2941733; } - if (typeof this.teams[teamName] !== 'undefined') { - const num = this.teams[teamName].githubTeamNumber; + // if (typeof this.teams[teamName] !== 'undefined') { + if (this.teams.has(teamName) === true) { + const num = this.teams.get(teamName).githubTeamNumber; Log.info("TestGitHubActions::getTeamNumber( " + teamName + " ) - returning: " + num); return Number(num); } @@ -2063,9 +2070,10 @@ export class TestGitHubActions implements IGitHubActions { } public async getTeam(teamNumber: number): Promise { - for (const team of this.teams) { + for (const teamName of this.teams.keys()) { + const team = this.teams.get(teamName); if (team.githubTeamNumber === teamNumber) { - return {githubTeamNumber: teamNumber, teamName: team.id}; + return {githubTeamNumber: teamNumber, teamName: team.teamName}; } } return null; @@ -2120,51 +2128,49 @@ export class TestGitHubActions implements IGitHubActions { } } - public async listPeople(): Promise> { + public async listPeople(): Promise { Log.info("TestGitHubActions::listPeople(..)"); - const people = []; + const people: GitPersonTuple[] = []; const start = Date.now(); - people.push({personNumber: start, url: 'URL', githubId: Test.REALBOTNAME1}); - people.push({personNumber: start - 5, url: 'URL', githubId: Test.REALUSERNAME}); - people.push({personNumber: start - 15, url: 'URL', githubId: Test.REALBOTNAME1}); - people.push({personNumber: start - 15, url: 'URL', githubId: Test.REALUSER1.github}); - people.push({personNumber: start - 15, url: 'URL', githubId: Test.REALUSER2.github}); - people.push({personNumber: start - 15, url: 'URL', githubId: Test.ADMIN1.github}); - people.push({personNumber: start - 25, url: 'URL', githubId: Test.USER1.github}); - people.push({personNumber: start - 35, url: 'URL', githubId: Test.USER2.github}); - people.push({personNumber: start - 45, url: 'URL', githubId: Test.USER3.github}); - people.push({personNumber: start - 55, url: 'URL', githubId: Test.USER4.github}); + people.push({githubPersonNumber: start, url: 'URL', githubId: Test.REALBOTNAME1}); + people.push({githubPersonNumber: start - 5, url: 'URL', githubId: Test.REALUSERNAME}); + people.push({githubPersonNumber: start - 15, url: 'URL', githubId: Test.REALBOTNAME1}); + people.push({githubPersonNumber: start - 15, url: 'URL', githubId: Test.REALUSER1.github}); + people.push({githubPersonNumber: start - 15, url: 'URL', githubId: Test.REALUSER2.github}); + people.push({githubPersonNumber: start - 15, url: 'URL', githubId: Test.ADMIN1.github}); + people.push({githubPersonNumber: start - 25, url: 'URL', githubId: Test.USER1.github}); + people.push({githubPersonNumber: start - 35, url: 'URL', githubId: Test.USER2.github}); + people.push({githubPersonNumber: start - 45, url: 'URL', githubId: Test.USER3.github}); + people.push({githubPersonNumber: start - 55, url: 'URL', githubId: Test.USER4.github}); return people; } - public async listRepos(): Promise> { + public async listRepos(): Promise { Log.info("TestGitHubActions::listRepos(..)"); const ret = []; for (const name of Object.keys(this.repos)) { const repo = this.repos[name]; - ret.push({repoNumber: Date.now(), repoName: name, url: repo}); + ret.push({githubRepoNumber: Date.now(), repoName: name, url: repo}); } Log.info("TestGitHubActions::listRepos(..) - #: " + ret.length + "; content: " + JSON.stringify(ret)); return ret; } - // Map teamName: {teamObject} - private teams: any = { - staff: {id: TeamController.STAFF_NAME, teamName: TeamController.STAFF_NAME, githubTeamNumber: '1000'}, - admin: {id: TeamController.ADMIN_NAME, teamName: TeamController.ADMIN_NAME, githubTeamNumber: '1001'} - }; - - // TODO: use a private teams map to keep track of teams - public async listTeams(): Promise> { + /** + * Returns the team tuples from the cache. + * + */ + public async listTeams(): Promise { Log.info("TestGitHubActions::listTeams(..)"); // return [{teamNumber: Date.now(), teamName: Test.TEAMNAME1}]; const ret = []; for (const name of Object.keys(this.teams)) { - const teamNum = this.teams[name].githubTeamNumber; - const teamName = this.teams[name].teamName; - ret.push({teamNumber: teamNum, teamName: teamName}); + const t = this.teams.get(name); + // const teamNum = this.teams[name].githubTeamNumber; + // const teamName = this.teams[name].teamName; + ret.push({githubTeamNumber: t.githubTeamNumber, teamName: t.teamName}); } Log.info("TestGitHubActions::listTeams(..) - #: " + ret.length + "; content: " + JSON.stringify(ret)); return ret; diff --git a/packages/portal/backend/src/controllers/GitHubController.ts b/packages/portal/backend/src/controllers/GitHubController.ts index 843fcc121..7c2e12115 100644 --- a/packages/portal/backend/src/controllers/GitHubController.ts +++ b/packages/portal/backend/src/controllers/GitHubController.ts @@ -37,6 +37,18 @@ export interface IGitHubController { releaseRepository(repo: Repository, teams: Team[], asCollaborators?: boolean): Promise; } +export interface GitPersonTuple { + githubId: string; + githubPersonNumber: number; + url: string; +} + +export interface GitRepoTuple { + repoName: string; + githubRepoNumber: number; + url: string; +} + export interface GitTeamTuple { teamName: string; githubTeamNumber: number; @@ -75,6 +87,7 @@ export class GitHubController implements IGitHubController { public async getTeamUrl(team: Team): Promise { const c = Config.getInstance(); + // GET /orgs/:org/teams/:team_slug const teamUrl = c.getProp(ConfigKey.githubHost) + '/orgs/' + c.getProp(ConfigKey.org) + '/teams/' + team.id; Log.info("GitHubController::getTeamUrl( " + team.id + " ) - URL: " + teamUrl); return teamUrl; @@ -134,15 +147,17 @@ export class GitHubController implements IGitHubController { try { // still add staff team with push, just not students Log.trace("GitHubController::createRepository() - add staff team to repo"); - const staffTeamNumber = await this.tc.getTeamNumber(TeamController.STAFF_NAME); - Log.trace('GitHubController::createRepository(..) - staffTeamNumber: ' + staffTeamNumber); - const staffAdd = await this.gha.addTeamToRepo(staffTeamNumber, repoName, 'admin'); + // const staffTeamNumber = await this.tc.getTeamNumber(TeamController.STAFF_NAME); + // Log.trace('GitHubController::createRepository(..) - staffTeamNumber: ' + staffTeamNumber); + // const staffAdd = await this.gha.addTeamToRepo(staffTeamNumber, repoName, 'admin'); + const staffAdd = await this.gha.addTeamToRepo(TeamController.STAFF_NAME, repoName, 'admin'); Log.trace('GitHubController::createRepository(..) - team name: ' + staffAdd.teamName); Log.trace("GitHubController::createRepository() - add admin team to repo"); - const adminTeamNumber = await this.tc.getTeamNumber(TeamController.ADMIN_NAME); - Log.trace('GitHubController::createRepository(..) - adminTeamNumber: ' + adminTeamNumber); - const adminAdd = await this.gha.addTeamToRepo(adminTeamNumber, repoName, 'admin'); + // const adminTeamNumber = await this.tc.getTeamNumber(TeamController.ADMIN_NAME); + // Log.trace('GitHubController::createRepository(..) - adminTeamNumber: ' + adminTeamNumber); + // const adminAdd = await this.gha.addTeamToRepo(adminTeamNumber, repoName, 'admin'); + const adminAdd = await this.gha.addTeamToRepo(TeamController.ADMIN_NAME, repoName, 'admin'); Log.trace('GitHubController::createRepository(..) - team name: ' + adminAdd.teamName); // add webhooks @@ -202,7 +217,6 @@ export class GitHubController implements IGitHubController { } else { await this.checkDatabase(null, team.id); - const teamNum = await this.tc.getTeamNumber(team.id); // now, add the team to the repository @@ -302,7 +316,9 @@ export class GitHubController implements IGitHubController { if (teamValue.githubTeamNumber > 0) { // worked - team.URL = teamValue.URL; + + // team.URL = teamValue.URL; + team.URL = await this.getTeamUrl(team); team.githubId = teamValue.githubTeamNumber; team.custom.githubAttached = false; // attaching happens in release await dbc.writeTeam(team); @@ -327,14 +343,17 @@ export class GitHubController implements IGitHubController { } Log.trace("GitHubController::provisionRepository() - add staff team to repo"); - const staffTeamNumber = await tc.getTeamNumber(TeamController.STAFF_NAME); - Log.trace('GitHubController::provisionRepository(..) - staffTeamNumber: ' + staffTeamNumber); - const staffAdd = await this.gha.addTeamToRepo(staffTeamNumber, repoName, 'admin'); + // const staffTeamNumber = await tc.getTeamNumber(TeamController.STAFF_NAME); + // Log.trace('GitHubController::provisionRepository(..) - staffTeamNumber: ' + staffTeamNumber); + // const staffAdd = await this.gha.addTeamToRepo(staffTeamNumber, repoName, 'admin'); + const staffAdd = await this.gha.addTeamToRepo(TeamController.STAFF_NAME, repoName, 'admin'); Log.trace('GitHubController::provisionRepository(..) - team name: ' + staffAdd.teamName); - const adminTeamNumber = await tc.getTeamNumber(TeamController.ADMIN_NAME); - Log.trace('GitHubController::provisionRepository(..) - adminTeamNumber: ' + adminTeamNumber); - const adminAdd = await this.gha.addTeamToRepo(adminTeamNumber, repoName, 'admin'); + Log.trace("GitHubController::provisionRepository() - add admin team to repo"); + // const adminTeamNumber = await tc.getTeamNumber(TeamController.ADMIN_NAME); + // Log.trace('GitHubController::provisionRepository(..) - adminTeamNumber: ' + adminTeamNumber); + // const adminAdd = await this.gha.addTeamToRepo(adminTeamNumber, repoName, 'admin'); + const adminAdd = await this.gha.addTeamToRepo(TeamController.ADMIN_NAME, repoName, 'admin'); Log.trace('GitHubController::provisionRepository(..) - team name: ' + adminAdd.teamName); // add webhooks @@ -409,8 +428,8 @@ export class GitHubController implements IGitHubController { }); const options: RequestInit = { - method: 'POST', - agent: new http.Agent() + method: 'POST', + agent: new http.Agent() }; let result; From 22de25b8a75318ffbc23aeb8de411eb8b786000e Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sun, 18 Sep 2022 22:47:26 -0700 Subject: [PATCH 006/104] Additional GitHub code cleanups and doc improvements. --- .../portal/backend/src-util/InvokeAutoTest.ts | 2 +- packages/portal/backend/src/Factory.ts | 2 - .../backend/src/controllers/GitHubActions.ts | 169 ++++++++---------- .../test/controllers/GitHubActionSpec.ts | 12 +- 4 files changed, 78 insertions(+), 107 deletions(-) diff --git a/packages/portal/backend/src-util/InvokeAutoTest.ts b/packages/portal/backend/src-util/InvokeAutoTest.ts index 91ecae66e..844fdb696 100644 --- a/packages/portal/backend/src-util/InvokeAutoTest.ts +++ b/packages/portal/backend/src-util/InvokeAutoTest.ts @@ -127,7 +127,7 @@ export class InvokeAutoTest { const projectId = url.substring(url.lastIndexOf(org) + org.length, url.lastIndexOf('/commit/')); const sha = url.substring(url.lastIndexOf('/commit/') + 8); Log.info("Making invisible request for project: " + projectId + '; sha: ' + sha + '; URL: ' + url); - await gha.simulateWebookComment(projectId, sha, this.MSG); + await gha.simulateWebhookComment(projectId, sha, this.MSG); } else { let u = url; // update prefix from: https://HOST/CPSC310-2018W-T1/ --> https://HOST/api/v3/repos/CPSC310-2018W-T1/ diff --git a/packages/portal/backend/src/Factory.ts b/packages/portal/backend/src/Factory.ts index fdee5f130..c74d1dfec 100644 --- a/packages/portal/backend/src/Factory.ts +++ b/packages/portal/backend/src/Factory.ts @@ -7,8 +7,6 @@ import {GitHubActions} from "./controllers/GitHubActions"; import {GitHubController, IGitHubController} from "./controllers/GitHubController"; import IREST from "./server/IREST"; -import * as path from 'path'; - export class Factory { /** diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index 69bbed438..4b1bc18e3 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -29,10 +29,10 @@ export interface IGitHubActions { * * Also updates the Repository object in the datastore with the URL and cloneURL. * - * @param repoId The name of the repo. Must be unique within the organization. - * @returns {Promise} provisioned team URL + * @param repoName The name of the repo. Must be unique within the organization. + * @returns {Promise} provisioned repo URL */ - createRepo(repoId: string): Promise; + createRepo(repoName: string): Promise; /** * Deletes a repo from the organization. @@ -43,8 +43,8 @@ export interface IGitHubActions { deleteRepo(repoName: string): Promise; /** - * Checks if a repo exists or not. If the request fails for _ANY_ reason the failure will not - * be reported, only that the repo does not exist. + * Checks if a repo exists or not. If the request fails for _ANY_ reason + * the failure will not be reported, only that the repo does not exist. * * @param repoName * @returns {Promise} @@ -57,6 +57,7 @@ export interface IGitHubActions { * https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/ * * @param teamName: string + * @returns {Promise} */ deleteTeam(teamName: string): Promise; @@ -64,21 +65,21 @@ export interface IGitHubActions { * Deletes a team. * * @param teamName + * @returns {Promise} */ deleteTeamByName(teamName: string): Promise; /** - * * Gets all repos in an org. - * This is just a subset of the return, but it is the subset we actually use: - * @returns {Promise<{ id: number, name: string, url: string }[]>} + * + * @returns {Promise<{GitRepoTuple[]>} */ listRepos(): Promise; /** * Gets all people in an org. * - * @returns {Promise<{ id: number, type: string, url: string, name: string }[]>} + * @returns {Promise} * this is just a subset of the return, but it is the subset we actually use */ listPeople(): Promise; @@ -107,7 +108,7 @@ export interface IGitHubActions { addWebhook(repoName: string, webhookEndpoint: string): Promise; /** - * Creates a team for a groupName (e.g., cpsc310_team1). + * Creates a GitHub team (e.g., cpsc310_team1). * * @param teamName * @param permission 'admin', 'pull', 'push' // admin for staff, push for students @@ -135,6 +136,7 @@ export interface IGitHubActions { /** * NOTE: needs the team ID (number), not the team name (string)! + * TODO: This is the only method that still needs a numeric GitHub ID; teamName would be better * * @param teamId * @param repoName @@ -148,20 +150,21 @@ export interface IGitHubActions { * * Returns -1 if the team does not exist. * - * @param {string} teamName + * @param teamName * @returns {Promise} */ getTeamNumber(teamName: string): Promise; /** * Gets the list of users on a team. + * * NOTE: this used to take a number, but GitHub changed the team API in 2020. * https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/ * * Returns [] if the team does not exist or nobody is on the team. * * @param {string} teamName - * @returns {Promise} + * @returns {Promise} */ getTeamMembers(teamName: string): Promise; @@ -175,23 +178,23 @@ export interface IGitHubActions { addGithubAuthToken(url: string): string; - // reportStdErr(stderr: any, prefix: string): void; - /** * Adds a file with the data given, to the specified repository. - * If force is set to true, will overwrite old files - * @param {string} repoURL - name of repository - * @param {string} fileName - name of file to write - * @param {string} fileContent - the content of the file to write to repo - * @param {boolean} force - allow for overwriting of old files + * If force is set to true, will overwrite old files. + * + * @param repoURL - name of repository + * @param fileName - name of file to write + * @param fileContent - the content of the file to write to repo + * @param force - allow for overwriting of old files * @returns {Promise} - true if write was successful */ writeFileToRepo(repoURL: string, fileName: string, fileContent: string, force?: boolean): Promise; /** - * Changes permissions for all teams for the given repository - * @param {string} repoName - * @param {string} permissionLevel - one of: 'push' 'pull' + * Changes permissions for all teams for the given repository. + * + * @param repoName + * @param permissionLevel - one of: 'push' 'pull' * @returns {Promise} */ setRepoPermission(repoName: string, permissionLevel: string): Promise; @@ -206,20 +209,20 @@ export interface IGitHubActions { makeComment(url: string, message: string): Promise; /** - * Simulates a comment as if it were received by a webook (for silently invoking AutoTest) + * Simulates a comment as if it were received by a webhook (for silently invoking AutoTest). * - * @param {string} projectName - * @param {string} sha - * @param {string} message + * @param projectName + * @param sha + * @param message * @returns {Promise} */ - simulateWebookComment(projectName: string, sha: string, message: string): Promise; + simulateWebhookComment(projectName: string, sha: string, message: string): Promise; /** - * Returns a list of teamIds on a repo + * Returns a list of teams on a repo. * - * @param {string} repoId - * @returns {{teamId: string}[]} + * @param repoId + * @returns {Promise} */ getTeamsOnRepo(repoId: string): Promise; @@ -306,14 +309,14 @@ export class GitHubActions implements IGitHubActions { * * Also updates the Repository object in the datastore with the URL and cloneURL. * - * @param repoId The name of the repo. Must be unique within the organization. + * @param repoName The name of the repo. Must be unique within the organization. * @returns {Promise} provisioned team URL */ - public async createRepo(repoId: string): Promise { + public async createRepo(repoName: string): Promise { const start = Date.now(); try { - Log.info("GitHubAction::createRepo( " + repoId + " ) - start"); - await GitHubActions.checkDatabase(repoId, null); + Log.info("GitHubAction::createRepo( " + repoName + " ) - start"); + await GitHubActions.checkDatabase(repoName, null); const uri = this.apiPath + '/orgs/' + this.org + '/repos'; const options: RequestInit = { @@ -324,7 +327,7 @@ export class GitHubActions implements IGitHubActions { 'Accept': 'application/json' }, body: JSON.stringify({ - name: repoId, + name: repoName, // In Dev and Test, Github free Org Repos cannot be private. private: true, has_issues: true, @@ -334,18 +337,18 @@ export class GitHubActions implements IGitHubActions { }) }; - Log.info("GitHubAction::createRepo( " + repoId + " ) - making request"); + Log.info("GitHubAction::createRepo( " + repoName + " ) - making request"); const response = await fetch(uri, options); const body = await response.json(); - Log.info("GitHubAction::createRepo( " + repoId + " ) - request complete"); + Log.info("GitHubAction::createRepo( " + repoName + " ) - request complete"); const url = body.html_url; - Log.info("GitHubAction::createRepo( " + repoId + " ) - db start"); - const repo = await this.dc.getRepository(repoId); + Log.info("GitHubAction::createRepo( " + repoName + " ) - db start"); + const repo = await this.dc.getRepository(repoName); repo.URL = url; // only update this field in the existing Repository record repo.cloneURL = body.clone_url; // only update this field in the existing Repository record await this.dc.writeRepository(repo); - Log.info("GitHubAction::createRepo( " + repoId + " ) - db done"); + Log.info("GitHubAction::createRepo( " + repoName + " ) - db done"); Log.info("GitHubAction::createRepo(..) - success; URL: " + url + "; delaying to prep repo. Took: " + Util.took(start)); await Util.delay(this.PAUSE); @@ -429,11 +432,7 @@ export class GitHubActions implements IGitHubActions { public async deleteTeamByName(teamName: string): Promise { Log.info("GitHubAction::deleteTeamByName( " + this.org + ", " + teamName + " ) - start"); - // const teamNum = await this.getTeamNumber(teamName); // be conservative, don't use TeamController on purpose - // if (teamNum >= 0) { return await this.deleteTeam(teamName); - // } - // return false; } /** @@ -460,7 +459,6 @@ export class GitHubActions implements IGitHubActions { return false; } - // const uri = this.apiPath + '/teams/' + teamId; // DELETE /orgs/:org/teams/:team_slug const uri = this.apiPath + "/orgs/" + this.org + "/teams/" + teamName; const options: RequestInit = { @@ -495,7 +493,7 @@ export class GitHubActions implements IGitHubActions { * * Gets all repos in an org. * This is just a subset of the return, but it is the subset we actually use: - * @returns {Promise<{ id: number, name: string, url: string }[]>} + * @returns {Promise { Log.info("GitHubActions::listRepos(..) - start"); @@ -515,8 +513,7 @@ export class GitHubActions implements IGitHubActions { const raw: any = await this.handlePagination(uri, options); - // const rows: Array<{ repoName: string, repoNumber: number, url: string }> = []; - const rows: GitRepoTuple[] = []; // Array<{ repoName: string, repoNumber: number, url: string }> = []; + const rows: GitRepoTuple[] = []; for (const entry of raw) { const id = entry.id; const name = entry.name; @@ -566,7 +563,6 @@ export class GitHubActions implements IGitHubActions { private async handlePagination(uri: string, options: RequestInit): Promise { Log.trace("GitHubActions::handlePagination(..) - start; PAGE_SIZE: " + this.pageSize); - const start = Date.now(); try { @@ -647,9 +643,8 @@ export class GitHubActions implements IGitHubActions { * * @returns {Promise<{id: number, name: string}[]>} */ - // public async listTeams(): Promise> { public async listTeams(): Promise { - // Log.info("GitHubActions::listTeams(..) - start"); + // Log.trace("GitHubActions::listTeams(..) - start"); const start = Date.now(); // per_page max is 100; 10 is useful for testing pagination though @@ -698,7 +693,6 @@ export class GitHubActions implements IGitHubActions { public async addWebhook(repoName: string, webhookEndpoint: string): Promise { Log.info("GitHubAction::addWebhook( " + repoName + ", " + webhookEndpoint + " ) - start"); - // Log.info("GitHubAction::addWebhook( .. ) - webhook: " + webhookEndpoint); let secret = Config.getInstance().getProp(ConfigKey.autotestSecret); secret = crypto.createHash('sha256').update(secret, 'utf8').digest('hex'); // webhook w/ sha256 @@ -736,9 +730,7 @@ export class GitHubActions implements IGitHubActions { Log.info("GitHubAction::updateWebhook( " + repoName + ", " + webhookEndpoint + " ) - start"); const existingWebhooks = await this.listWebhooks(repoName); - if (existingWebhooks.length === 1) { - const hookId = (existingWebhooks[0] as any).id; let secret = Config.getInstance().getProp(ConfigKey.autotestSecret); @@ -785,9 +777,9 @@ export class GitHubActions implements IGitHubActions { * * @param teamName * @param permission 'admin', 'pull', 'push' // admin for staff, push for students - * @returns {Promise} team tuple + * @returns {Promise} team tuple */ - public async createTeam(teamName: string, permission: string): Promise<{ teamName: string, githubTeamNumber: number, URL: string }> { + public async createTeam(teamName: string, permission: string): Promise { Log.info("GitHubAction::teamCreate( " + this.org + ", " + teamName + ", " + permission + ", ... ) - start"); const start = Date.now(); @@ -797,9 +789,7 @@ export class GitHubActions implements IGitHubActions { const teamNum = await this.getTeamNumber(teamName); // be conservative, don't use TeamController on purpose if (teamNum > 0) { Log.info("GitHubAction::teamCreate( " + teamName + ", ... ) - success; exists: " + teamNum); - const config = Config.getInstance(); - const url = config.getProp(ConfigKey.githubHost) + "/orgs/" + config.getProp(ConfigKey.org) + "/teams/" + teamName; - return {teamName: teamName, githubTeamNumber: teamNum, URL: url}; + return {teamName: teamName, githubTeamNumber: teamNum}; } else { Log.info('GitHubAction::teamCreate( ' + teamName + ', ... ) - does not exist; creating'); const uri = this.apiPath + '/orgs/' + this.org + '/teams'; @@ -817,17 +807,13 @@ export class GitHubActions implements IGitHubActions { }; const response = await fetch(uri, options); const body = await response.json(); - - const config = Config.getInstance(); - const url = config.getProp(ConfigKey.githubHost) + "/orgs/" + config.getProp(ConfigKey.org) + "/teams/" + teamName; - // TODO: simplify callees by setting Team.URL here and persisting it (like we do with createRepo) Log.info("GitHubAction::teamCreate(..) - success; new: " + body.id + "; took: " + Util.took(start)); // remove default token provider/maintainer from team await this.removeMembersFromTeam(teamName, [Config.getInstance().getProp(ConfigKey.githubBotName)]); - return {teamName: teamName, githubTeamNumber: body.id, URL: url}; + return {teamName: teamName, githubTeamNumber: body.id}; } } catch (err) { // explicitly log this failure @@ -950,7 +936,6 @@ export class GitHubActions implements IGitHubActions { const start = Date.now(); try { const uri = this.apiPath + '/teams/' + teamId + '/repos/' + this.org + '/' + repoName; - // Log.info("GitHubAction::addTeamToRepo(..) - URI: " + uri); const options: RequestInit = { method: 'PUT', headers: { @@ -1023,7 +1008,6 @@ export class GitHubActions implements IGitHubActions { * @returns {Promise} */ public async getTeamMembers(teamName: string): Promise { - // public async getTeamMembers(teamNumber: number): Promise { Log.info("GitHubAction::getTeamMembers( " + teamName + " ) - start"); if (teamName === null) { @@ -1032,8 +1016,6 @@ export class GitHubActions implements IGitHubActions { const start = Date.now(); try { - // deprecated: https://developer.github.com/changes/2020-01-21-moving-the-team-api-endpoints/ - // const uri = this.apiPath + '/teams/' + teamNumber + '/members?per_page=' + this.pageSize; // /orgs/:org/teams/:team_slug const uri = this.apiPath + "/orgs/" + this.org + "/teams/" + teamName + "/members"; const options: RequestInit = { @@ -1080,7 +1062,6 @@ export class GitHubActions implements IGitHubActions { * @returns {Promise} */ public async getTeam(teamNumber: number): Promise { - Log.info("GitHubAction::getTeam( " + teamNumber + " ) - start"); if (teamNumber === null) { @@ -1132,9 +1113,6 @@ export class GitHubActions implements IGitHubActions { await GitHubActions.checkDatabase(null, teamName); } - const tc = new TeamController(); - // const teamNumber = await tc.getTeamNumber(teamName); // try to use cache - // const teamNumber = await gh.getTeamNumber(teamName); const teamMembers = await gh.getTeamMembers(teamName); for (const member of teamMembers) { if (member === userName) { @@ -1153,8 +1131,6 @@ export class GitHubActions implements IGitHubActions { Log.info('GitHubAction::listTeamMembers( ' + teamName + ' ) - start'); const gh = this; - // const teamNumber = await new TeamController().getTeamNumber(teamName); - // const teamMembers = await gh.getTeamMembers(teamNumber); const teamMembers = await gh.getTeamMembers(teamName); return teamMembers; @@ -1745,20 +1721,20 @@ export class GitHubActions implements IGitHubActions { return true; } - public async simulateWebookComment(projectName: string, sha: string, message: string): Promise { + public async simulateWebhookComment(projectName: string, sha: string, message: string): Promise { try { if (typeof projectName === "undefined" || projectName === null) { - Log.error("GitHubActions::simulateWebookComment(..) - url is required"); + Log.error("GitHubActions::simulateWebhookComment(..) - url is required"); return Promise.resolve(false); } if (typeof sha === "undefined" || sha === null) { - Log.error("GitHubActions::simulateWebookComment(..) - sha is required"); + Log.error("GitHubActions::simulateWebhookComment(..) - sha is required"); return Promise.resolve(false); } if (typeof message === "undefined" || message === null) { - Log.error("GitHubActions::simulateWebookComment(..) - message is required"); + Log.error("GitHubActions::simulateWebhookComment(..) - message is required"); return Promise.resolve(false); } @@ -1771,7 +1747,7 @@ export class GitHubActions implements IGitHubActions { messageToPrint = messageToPrint.substr(0, 80) + "..."; } - Log.info("GitHubActions::simulateWebookComment(..) - Simulating comment to project: " + + Log.info("GitHubActions::simulateWebhookComment(..) - Simulating comment to project: " + projectName + "; sha: " + sha + "; message: " + messageToPrint); const c = Config.getInstance(); @@ -1799,7 +1775,7 @@ export class GitHubActions implements IGitHubActions { }; const urlToSend = Config.getInstance().getProp(ConfigKey.publichostname) + '/portal/githubWebhook'; - Log.info("GitHubService::simulateWebookComment(..) - url: " + urlToSend + "; body: " + JSON.stringify(body)); + Log.info("GitHubService::simulateWebhookComment(..) - url: " + urlToSend + "; body: " + JSON.stringify(body)); const options: RequestInit = { method: "POST", @@ -1818,19 +1794,19 @@ export class GitHubActions implements IGitHubActions { // const url = url; // this url comes from postbackURL which uses the right API format try { const res = await fetch(urlToSend, options); // .then(function(res) { - Log.trace("GitHubService::simulateWebookComment(..) - success"); // : " + res); + Log.trace("GitHubService::simulateWebhookComment(..) - success"); // : " + res); return Promise.resolve(true); } catch (err) { - Log.error("GitHubService::simulateWebookComment(..) - ERROR: " + err); + Log.error("GitHubService::simulateWebhookComment(..) - ERROR: " + err); return Promise.resolve(false); } } else { - Log.trace("GitHubService::simulateWebookComment(..) - send skipped (config.postback === false)"); + Log.trace("GitHubService::simulateWebhookComment(..) - send skipped (config.postback === false)"); return Promise.resolve(true); } } catch (err) { - Log.error("GitHubService::simulateWebookComment(..) - ERROR: " + err); + Log.error("GitHubService::simulateWebhookComment(..) - ERROR: " + err); return Promise.resolve(false); } } @@ -1968,17 +1944,17 @@ export class TestGitHubActions implements IGitHubActions { private repos: any = {}; - public async createRepo(repoId: string): Promise { - Log.info("TestGitHubActions::createRepo( " + repoId + " ) - start"); - await GitHubActions.checkDatabase(repoId, null); + public async createRepo(repoName: string): Promise { + Log.info("TestGitHubActions::createRepo( " + repoName + " ) - start"); + await GitHubActions.checkDatabase(repoName, null); - if (typeof this.repos[repoId] === 'undefined') { - Log.info("TestGitHubActions::createRepo( " + repoId + " ) - created"); + if (typeof this.repos[repoName] === 'undefined') { + Log.info("TestGitHubActions::createRepo( " + repoName + " ) - created"); const c = Config.getInstance(); - this.repos[repoId] = c.getProp(ConfigKey.githubHost) + '/' + c.getProp(ConfigKey.org) + '/' + repoId; + this.repos[repoName] = c.getProp(ConfigKey.githubHost) + '/' + c.getProp(ConfigKey.org) + '/' + repoName; } - Log.info("TestGitHubActions::createRepo( " + repoId + " ) - repos: " + JSON.stringify(this.repos)); - return this.repos[repoId]; + Log.info("TestGitHubActions::createRepo( " + repoName + " ) - repos: " + JSON.stringify(this.repos)); + return this.repos[repoName]; } // public async createTeam(teamName: string, permission: string): Promise<{ teamName: string; githubTeamNumber: number; URL: string }> { @@ -2149,7 +2125,7 @@ export class TestGitHubActions implements IGitHubActions { public async listRepos(): Promise { Log.info("TestGitHubActions::listRepos(..)"); - const ret = []; + const ret: GitRepoTuple[] = []; for (const name of Object.keys(this.repos)) { const repo = this.repos[name]; ret.push({githubRepoNumber: Date.now(), repoName: name, url: repo}); @@ -2164,12 +2140,9 @@ export class TestGitHubActions implements IGitHubActions { */ public async listTeams(): Promise { Log.info("TestGitHubActions::listTeams(..)"); - // return [{teamNumber: Date.now(), teamName: Test.TEAMNAME1}]; const ret = []; for (const name of Object.keys(this.teams)) { const t = this.teams.get(name); - // const teamNum = this.teams[name].githubTeamNumber; - // const teamName = this.teams[name].teamName; ret.push({githubTeamNumber: t.githubTeamNumber, teamName: t.teamName}); } Log.info("TestGitHubActions::listTeams(..) - #: " + ret.length + "; content: " + JSON.stringify(ret)); @@ -2245,8 +2218,8 @@ export class TestGitHubActions implements IGitHubActions { return; } - public simulateWebookComment(projectName: string, sha: string, message: string): Promise { - Log.info("TestGitHubActions::simulateWebookComment(..)"); + public simulateWebhookComment(projectName: string, sha: string, message: string): Promise { + Log.info("TestGitHubActions::simulateWebhookComment(..)"); return; } diff --git a/packages/portal/backend/test/controllers/GitHubActionSpec.ts b/packages/portal/backend/test/controllers/GitHubActionSpec.ts index f665455f7..55a5a1023 100644 --- a/packages/portal/backend/test/controllers/GitHubActionSpec.ts +++ b/packages/portal/backend/test/controllers/GitHubActionSpec.ts @@ -733,30 +733,30 @@ describe("GitHubActions", () => { }).timeout(TIMEOUT); it("Should not be possible to simulate a webhook with the wrong params.", async function () { - let worked = await gh.simulateWebookComment(null, "SHA", "message"); + let worked = await gh.simulateWebhookComment(null, "SHA", "message"); expect(worked).to.be.false; - worked = await gh.simulateWebookComment(REPONAME, null, "message"); + worked = await gh.simulateWebhookComment(REPONAME, null, "message"); expect(worked).to.be.false; - worked = await gh.simulateWebookComment(REPONAME, "SHA", null); + worked = await gh.simulateWebhookComment(REPONAME, "SHA", null); expect(worked).to.be.false; }).timeout(TIMEOUT); it("Should be possible to simulate a webhook.", async function () { - let worked = await gh.simulateWebookComment(Test.REPONAMEREAL_POSTTEST, "SHA", "message"); + let worked = await gh.simulateWebhookComment(Test.REPONAMEREAL_POSTTEST, "SHA", "message"); expect(worked).to.be.false; // SHA is not right let ex = null; try { let msg = "message"; - worked = await gh.simulateWebookComment(Test.REPONAMEREAL_POSTTEST, "c35a0e5968338a9757813b58368f36ddd64b063e", msg); + worked = await gh.simulateWebhookComment(Test.REPONAMEREAL_POSTTEST, "c35a0e5968338a9757813b58368f36ddd64b063e", msg); for (let i = 0; i < 10; i++) { msg = msg + msg; // make a long message } msg = msg + '\n' + msg; - worked = await gh.simulateWebookComment(Test.REPONAMEREAL_POSTTEST, "c35a0e5968338a9757813b58368f36ddd64b063e", msg); + worked = await gh.simulateWebhookComment(Test.REPONAMEREAL_POSTTEST, "c35a0e5968338a9757813b58368f36ddd64b063e", msg); // NOTE: worked not checked because githubWebhook needs to be active for this to work // expect(worked).to.be.true; From 77b1655edf1d16c85d1b304a9e2544b024b3b484 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Mon, 19 Sep 2022 15:16:27 -0700 Subject: [PATCH 007/104] More dev docs + new tests for issues and branch protection rules --- .env.sample | 2 + docs/developer/bootstrap.md | 7 + packages/common/TestHarness.ts | 255 ++++++++++-------- .../src/controllers/GitHubController.ts | 8 + .../test/controllers/GitHubControllerSpec.ts | 92 ++++++- .../controllers/RepositoryControllerSpec.ts | 2 +- .../backend/test/server/AdminRoutesSpec.ts | 46 ++++ .../backend/test/server/GeneralRoutesSpec.ts | 33 --- 8 files changed, 285 insertions(+), 160 deletions(-) diff --git a/.env.sample b/.env.sample index d9f7f93e6..f5ce0fa43 100644 --- a/.env.sample +++ b/.env.sample @@ -154,10 +154,12 @@ AUTOTEST_POSTBACK=false ## Where the AutoTest service should store persistent data (e.g. grade container execution logs) ## This path is on the HOST machine (and is the mount point for PERSIST_DIR inside the grade container) +## If testing, setting within /tmp should work HOST_DIR=/var/opt/classy ## Where the AutoTest service should store persistent data (e.g. grade container execution logs) ## This path is INSIDE the container (and is bound to HOST_DIR on the host machine) +## If testing, set as relative path (will persist within HOST_DIR) PERSIST_DIR=/output diff --git a/docs/developer/bootstrap.md b/docs/developer/bootstrap.md index 8cb010191..84b52a1e0 100644 --- a/docs/developer/bootstrap.md +++ b/docs/developer/bootstrap.md @@ -49,6 +49,13 @@ The most common of these services can be invoked from the `classy/` directory th Some handy dev scripts also exist; these can be found in `portal/backend/src-util/`; use these with care, many modify the database or GitHub repos in unrecoverable ways. +The automated test suite is stored in: +* `packages/autotest/test/` +* `packages/portal/backend/test/` + +To run these in the IDE create a Mocha target in Webstorm with `-r tsconfig-paths/register` as the node options and `--exit` as the mocha options. +To run these on the terminal, execute `yarn run test` in `packages/autotest/` or `packages/portal/backend/` + ## QA Checklist More checks may need to be made depending on the nature of your work, but these are the recommended checks: diff --git a/packages/common/TestHarness.ts b/packages/common/TestHarness.ts index b7a2e04e7..adac9ac4e 100644 --- a/packages/common/TestHarness.ts +++ b/packages/common/TestHarness.ts @@ -6,7 +6,17 @@ import {PersonController} from "../portal/backend/src/controllers/PersonControll import {RepositoryController} from "../portal/backend/src/controllers/RepositoryController"; import {TeamController} from "../portal/backend/src/controllers/TeamController"; import {Factory} from "../portal/backend/src/Factory"; -import {Auth, Course, Deliverable, Grade, Person, PersonKind, Repository, Result, Team} from "../portal/backend/src/Types"; +import { + Auth, + Course, + Deliverable, + Grade, + Person, + PersonKind, + Repository, + Result, + Team +} from "../portal/backend/src/Types"; import Config, {ConfigKey} from "./Config"; import Log from "./Log"; import {ContainerInput, ContainerOutput, ContainerState} from "./types/ContainerTypes"; @@ -267,12 +277,12 @@ export class Test { // NOTE: see FrontendDatasetGenerator for ideas const grade: GradePayload = { - score: 100, - comment: 'comment', - urlName: 'urlName', - URL: 'URL', + score: 100, + comment: 'comment', + urlName: 'urlName', + URL: 'URL', timestamp: new Date(Date.UTC(2018, 1, 1, 1, 1)).getTime(), - custom: {} + custom: {} }; const gc = new GradesController(); @@ -313,25 +323,25 @@ export class Test { let auth: Auth = { personId: Test.USER1.id, - token: Test.REALTOKEN + token: Test.REALTOKEN }; await dc.writeAuth(auth); auth = { personId: Test.GITHUB1.id, - token: Test.REALTOKEN + token: Test.REALTOKEN }; await dc.writeAuth(auth); auth = { personId: Test.ADMIN1.id, - token: Test.REALTOKEN + token: Test.REALTOKEN }; await dc.writeAuth(auth); auth = { personId: Test.ADMINSTAFF1.id, - token: Test.REALTOKEN + token: Test.REALTOKEN }; await dc.writeAuth(auth); } @@ -340,29 +350,29 @@ export class Test { const d: Deliverable = { id: delivId, - URL: 'http://NOTSET', - openTimestamp: new Date(1400000000000).getTime(), + URL: 'http://NOTSET', + openTimestamp: new Date(1400000000000).getTime(), closeTimestamp: new Date(1500000000000).getTime(), gradesReleased: false, - shouldProvision: true, - importURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/classytest/PostTestDoNotDelete.git', - teamMinSize: 2, - teamMaxSize: 2, - teamSameLab: true, + shouldProvision: true, + importURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/classytest/PostTestDoNotDelete.git', + teamMinSize: 2, + teamMaxSize: 2, + teamSameLab: true, teamStudentsForm: true, - teamPrefix: 't', - repoPrefix: '', + teamPrefix: 't', + repoPrefix: '', visibleToStudents: true, - lateAutoTest: false, - shouldAutoTest: true, - autotest: { - dockerImage: 'testImage', - studentDelay: 60 * 60 * 12, // 12h - maxExecTime: 300, + lateAutoTest: false, + shouldAutoTest: true, + autotest: { + dockerImage: 'testImage', + studentDelay: 60 * 60 * 12, // 12h + maxExecTime: 300, regressionDelivIds: [], - custom: {} + custom: {} }, rubric: {}, @@ -377,26 +387,26 @@ export class Test { personId, delivId, score, - comment: 'comment', + comment: 'comment', timestamp: Date.now(), - urlName: 'name', - URL: 'URL', - custom: {} + urlName: 'name', + URL: 'URL', + custom: {} }; return grade; } public static createPerson(id: string, csId: string, githubId: string, kind: PersonKind | null): Person { const p: Person = { - id: id, - csId: csId, - githubId: githubId, + id: id, + csId: csId, + githubId: githubId, studentNumber: null, fName: 'first_' + id, lName: 'last_' + id, - kind: kind, - URL: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + githubId, + kind: kind, + URL: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + githubId, labId: null, @@ -405,6 +415,21 @@ export class Test { return p; } + /** + * Expose CI check. Some tests require infrastructure that will not + * be accessible from individual dev machines. + */ + public static isCI(): boolean { + const ci = process.env.CI; + if (typeof ci !== "undefined" && Boolean(ci) === true) { + Log.test("Test::isCI() - running in CI"); + return true; + } else { + Log.test("Test::isCI() - not running CI"); + return false; + } + } + /** * Determines whether slow tests should be executed. They will _always_ run on CI, but * you can also set override = true to execute them locally. This is important if you @@ -414,9 +439,7 @@ export class Test { * @returns {boolean} */ public static runSlowTest() { - - const ci = process.env.CI; - if (Factory.OVERRIDE || typeof ci !== 'undefined' && Boolean(ci) === true) { + if (Factory.OVERRIDE || Test.isCI() === true) { Log.test("Test::runSlowTest() - running in CI or overriden; not skipping"); return true; } else { @@ -440,8 +463,8 @@ export class Test { public static getConfigUser(userKey: ConfigKey, num: number = null): any { const username = num ? Config.getInstance().getProp(userKey).split(',')[num - 1].trim() : Config.getInstance().getProp(userKey); return { - id: username + 'ID', - csId: username + 'CSID', + id: username + 'ID', + csId: username + 'CSID', github: username }; } @@ -504,47 +527,47 @@ export class Test { const deliv: Deliverable = { id: delivId, - URL: 'https://NOTSET', - openTimestamp: -1, - closeTimestamp: -1, - gradesReleased: false, + URL: 'https://NOTSET', + openTimestamp: -1, + closeTimestamp: -1, + gradesReleased: false, // delay: -1, - shouldProvision: false, - importURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/classytest/' + Test.REPONAMEREAL_POSTTEST + '.git', - teamMinSize: 1, - teamMaxSize: 1, - teamSameLab: false, - teamStudentsForm: false, - teamPrefix: 'team', - repoPrefix: '', + shouldProvision: false, + importURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/classytest/' + Test.REPONAMEREAL_POSTTEST + '.git', + teamMinSize: 1, + teamMaxSize: 1, + teamSameLab: false, + teamStudentsForm: false, + teamPrefix: 'team', + repoPrefix: '', // bootstrapUrl: '', - lateAutoTest: false, - shouldAutoTest: true, - autotest: { - dockerImage: 'testImage', - studentDelay: 60 * 60 * 12, // 12h - maxExecTime: 300, + lateAutoTest: false, + shouldAutoTest: true, + autotest: { + dockerImage: 'testImage', + studentDelay: 60 * 60 * 12, // 12h + maxExecTime: 300, regressionDelivIds: [], - custom: {} + custom: {} }, visibleToStudents: true, - rubric: {}, - custom: {} + rubric: {}, + custom: {} }; return Util.clone(deliv) as Deliverable; } public static getPerson(id: string): Person { const p: Person = { - id: id, - csId: id, - githubId: id, + id: id, + csId: id, + githubId: id, studentNumber: null, fName: 'f' + id, lName: 'l' + id, - kind: null, - URL: null, + kind: null, + URL: null, labId: null, @@ -556,14 +579,14 @@ export class Test { public static getGrade(delivId: string, personId: string, score: number): Grade { const grade: Grade = { personId: personId, - delivId: delivId, + delivId: delivId, - score: score, - comment: '', + score: score, + comment: '', timestamp: Date.now(), urlName: 'urlName', - URL: 'url', + URL: 'url', custom: {} }; @@ -572,28 +595,28 @@ export class Test { public static getTeam(teamId: string, delivId: string, people: string[]): Team { const team: Team = { - id: teamId, - delivId: delivId, - githubId: null, + id: teamId, + delivId: delivId, + githubId: null, // URL: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + // Config.getInstance().getProp(ConfigKey.org) + '/teams/' + teamId, // repoName: null, // repoUrl: null, - URL: null, + URL: null, personIds: people, - custom: {} + custom: {} }; return Util.clone(team) as Team; } public static getRepository(id: string, delivId: string, teamId: string): Repository { const repo: Repository = { - id: id, - delivId: delivId, - URL: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + id, + id: id, + delivId: delivId, + URL: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + id, cloneURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + id + '.git', - teamIds: [teamId], - custom: {} + teamIds: [teamId], + custom: {} }; return Util.clone(repo) as Repository; } @@ -601,9 +624,9 @@ export class Test { public static createCourseRecord(): Course { const courseId = Config.getInstance().getProp(ConfigKey.name); const out: Course = { - id: courseId, + id: courseId, defaultDeliverableId: null, - custom: {} + custom: {} }; return out; } @@ -639,66 +662,66 @@ export class Test { const output: ContainerOutput = { // commitURL: commitURL, - timestamp: ts, - report: { + timestamp: ts, + report: { scoreOverall: score, - scoreTest: Math.random() * 100, - scoreCover: Math.random() * 100, - passNames: passNames, - failNames: failNames, - errorNames: errorNames, - skipNames: skipNames, - custom: {}, - feedback: 'feedback', - result: 'SUCCESS', - attachments: [] + scoreTest: Math.random() * 100, + scoreCover: Math.random() * 100, + passNames: passNames, + failNames: failNames, + errorNames: errorNames, + skipNames: skipNames, + custom: {}, + feedback: 'feedback', + result: 'SUCCESS', + attachments: [] }, postbackOnComplete: true, - custom: {}, - state: ContainerState.SUCCESS, - graderTaskId: "" + custom: {}, + state: ContainerState.SUCCESS, + graderTaskId: "" }; const input: ContainerInput = { - target: { + target: { delivId: delivId, - repoId: repoId, + repoId: repoId, // branch: 'master', - cloneURL: 'cloneURL', + cloneURL: 'cloneURL', commitSHA: sha, commitURL: commitURL, botMentioned: false, - personId: null, - kind: 'push', + personId: null, + kind: 'push', // projectURL: projectURL, postbackURL: 'postbackURL', - timestamp: ts + timestamp: ts }, containerConfig: { - dockerImage: "imageName", - studentDelay: 300, - maxExecTime: 6000, + dockerImage: "imageName", + studentDelay: 300, + maxExecTime: 6000, regressionDelivIds: [], - custom: {}, - openTimestamp: 0, - closeTimestamp: 10000, - lateAutoTest: true, + custom: {}, + openTimestamp: 0, + closeTimestamp: 10000, + lateAutoTest: true, }, - delivId: delivId, + delivId: delivId, }; const result: Result = { - delivId: delivId, - repoId: repoId, + delivId: delivId, + repoId: repoId, // timestamp: ts, commitURL: commitURL, commitSHA: sha, - input: input, - output: output, - people: people + input: input, + output: output, + people: people }; return Util.clone(result) as Result; diff --git a/packages/portal/backend/src/controllers/GitHubController.ts b/packages/portal/backend/src/controllers/GitHubController.ts index 7c2e12115..fd22ecfac 100644 --- a/packages/portal/backend/src/controllers/GitHubController.ts +++ b/packages/portal/backend/src/controllers/GitHubController.ts @@ -382,6 +382,10 @@ export class GitHubController implements IGitHubController { } public async updateBranchProtection(repo: Repository, rules: BranchRule[]): Promise { + if (repo === null) { + throw new Error("GitHubController::updateBranchProtection(..) - null repo"); + } + Log.info("GitHubController::updateBranchProtection(", repo.id, ", ...) - start"); if (!await this.gha.repoExists(repo.id)) { throw new Error("GitHubController::updateBranchProtection() - " + repo.id + " did not exist"); @@ -393,6 +397,10 @@ export class GitHubController implements IGitHubController { } public async createIssues(repo: Repository, issues: Issue[]): Promise { + if (repo === null) { + throw new Error("GitHubController::createIssues(..) - null repo"); + } + Log.info("GitHubController::createIssues(", repo.id, ", ...) - start"); if (!await this.gha.repoExists(repo.id)) { throw new Error("GitHubController::createIssues() - " + repo.id + " did not exist"); diff --git a/packages/portal/backend/test/controllers/GitHubControllerSpec.ts b/packages/portal/backend/test/controllers/GitHubControllerSpec.ts index 381c2d507..9e73f2fa1 100644 --- a/packages/portal/backend/test/controllers/GitHubControllerSpec.ts +++ b/packages/portal/backend/test/controllers/GitHubControllerSpec.ts @@ -25,7 +25,7 @@ describe("GitHubController", () => { let gha: IGitHubActions; - before(async function() { + before(async function () { this.timeout(Test.TIMEOUTLONG); Log.test("GitHubControllerSpec::before() - start; forcing testorg"); @@ -74,7 +74,7 @@ describe("GitHubController", () => { Test.suiteAfter('GitHubController'); }); - beforeEach(function() { + beforeEach(function () { const exec = Test.runSlowTest(); if (exec === true) { Log.test("GitHubController::BeforeEach() - running in CI; not skipping"); @@ -85,7 +85,7 @@ describe("GitHubController", () => { } }); - it("Should be able to clear out prior result", async function() { + it("Should be able to clear out prior result", async function () { // not really a test, we just want something to run first we can set timeout on (cannot add timeout to before) Log.test("Clearing prior result"); try { @@ -121,7 +121,7 @@ describe("GitHubController", () => { expect(repoUrl).to.equal(url); }); - it("Should be able to provision a repo.", async function() { + it("Should be able to provision a repo.", async function () { const githubHost = Config.getInstance().getProp(ConfigKey.githubHost); const repos = await new RepositoryController().getAllRepos(); expect(repos.length).to.be.greaterThan(0); @@ -134,7 +134,7 @@ describe("GitHubController", () => { expect(provisioned).to.be.true; }).timeout(Test.TIMEOUTLONG); - it("Should fail to provision a repo that already exists.", async function() { + it("Should fail to provision a repo that already exists.", async function () { const githubHost = Config.getInstance().getProp(ConfigKey.githubHost); const repos = await new RepositoryController().getAllRepos(); expect(repos.length).to.be.greaterThan(0); @@ -166,7 +166,7 @@ describe("GitHubController", () => { }).timeout(Test.TIMEOUTLONG); - it("Should be able to create a repo.", async function() { + it("Should be able to create a repo.", async function () { // setup const rc: RepositoryController = new RepositoryController(); const repo = await rc.getRepository(Test.REPONAME2); @@ -194,7 +194,7 @@ describe("GitHubController", () => { // expect(ex).to.not.be.null; // }).timeout(Test.TIMEOUTLONG); - it("Should not be able to create a repo when preconditions are not met.", async function() { + it("Should not be able to create a repo when preconditions are not met.", async function () { // setup const rc: RepositoryController = new RepositoryController(); const repo = await rc.getRepository(Test.REPONAME2); @@ -228,7 +228,7 @@ describe("GitHubController", () => { }).timeout(Test.TIMEOUTLONG); - it("Should be able to create a repo with a custom path.", async function() { + it("Should be able to create a repo with a custom path.", async function () { // NOTE: this test is unreliable and needs to be fundamentally fixed this.skip(); @@ -252,7 +252,7 @@ describe("GitHubController", () => { expect(success).to.be.true; }).timeout(Test.TIMEOUTLONG); - it("Should be able to release a repo.", async function() { + it("Should be able to release a repo.", async function () { // setup const rc: RepositoryController = new RepositoryController(); const allRepos: Repository[] = await rc.getAllRepos(); @@ -273,7 +273,7 @@ describe("GitHubController", () => { expect(success).to.be.true; }).timeout(Test.TIMEOUT); - it("Should fail to release a repo if preconditions are not met.", async function() { + it("Should fail to release a repo if preconditions are not met.", async function () { // setup const rc: RepositoryController = new RepositoryController(); const allRepos: Repository[] = await rc.getAllRepos(); @@ -311,6 +311,78 @@ describe("GitHubController", () => { expect(ex).to.not.be.null; }).timeout(Test.TIMEOUT); + it("Should be update branch protection.", async function () { + await Test.prepareRepositories(); + + const rc: RepositoryController = new RepositoryController(); + const repo = await rc.getRepository(Test.REPONAME1); + expect(repo).to.not.be.null; + + if (await gha.repoExists(Test.REPONAME1) === false) { + // create repo + const url = await gha.createRepo(Test.REPONAME1); + expect(url).to.have.length.greaterThan(0); + } + const success = await gc.updateBranchProtection(repo, [{name: Test.USER1.github, reviews: 1}]); + expect(success).to.be.true; + }).timeout(Test.TIMEOUT); + + it("Should not update branch protection for a repo that does not exist.", async function () { + await Test.prepareRepositories(); + + const rc: RepositoryController = new RepositoryController(); + const repo = await rc.getRepository("repo_" + Date.now()); + expect(repo).to.be.null; + + let res = null; + let ex = null; + try { + // should throw + res = await gc.updateBranchProtection(repo, [{name: Test.USER1.github, reviews: 1}]); + } catch (err) { + ex = err; + } + expect(res).to.be.null; + expect(ex).to.not.be.null; + expect(ex.message).to.equal("GitHubController::updateBranchProtection(..) - null repo"); + }).timeout(Test.TIMEOUT); + + it("Should be create an issue.", async function () { + await Test.prepareRepositories(); + + const rc: RepositoryController = new RepositoryController(); + const repo = await rc.getRepository(Test.REPONAME1); + expect(repo).to.not.be.null; + + if (await gha.repoExists(Test.REPONAME1) === false) { + // create repo + const url = await gha.createRepo(Test.REPONAME1); + expect(url).to.have.length.greaterThan(0); + } + const success = await gc.createIssues(repo, [{title: "Issue Title", body: "Issue Body"}]); + expect(success).to.be.true; + }).timeout(Test.TIMEOUT); + + it("Should not create an issue for a repo that does not exist.", async function () { + await Test.prepareRepositories(); + + const rc: RepositoryController = new RepositoryController(); + const repo = await rc.getRepository("repo_" + Date.now()); + expect(repo).to.be.null; + + let res = null; + let ex = null; + try { + // should throw + res = await gc.createIssues(repo, [{title: "Issue Title", body: "Should not exist"}]); + } catch (err) { + ex = err; + } + expect(res).to.be.null; + expect(ex).to.not.be.null; + expect(ex.message).to.equal("GitHubController::createIssues(..) - null repo"); + }).timeout(Test.TIMEOUT); + // TODO: actually write tests for the PR feature // xit("Should fail to create a pull request.", async function() { // let res = null; diff --git a/packages/portal/backend/test/controllers/RepositoryControllerSpec.ts b/packages/portal/backend/test/controllers/RepositoryControllerSpec.ts index 470c774d2..979cb0cea 100644 --- a/packages/portal/backend/test/controllers/RepositoryControllerSpec.ts +++ b/packages/portal/backend/test/controllers/RepositoryControllerSpec.ts @@ -22,7 +22,7 @@ describe("RepositoryController", () => { before(async () => { await Test.suiteBefore('RepositoryController'); - // clear stale data (removed; happens in suitebefore) + // clear stale data (removed; happens in suiteBefore) // const dbc = DatabaseController.getInstance(); // await dbc.clearData(); diff --git a/packages/portal/backend/test/server/AdminRoutesSpec.ts b/packages/portal/backend/test/server/AdminRoutesSpec.ts index 8a2b804a2..374e836b4 100644 --- a/packages/portal/backend/test/server/AdminRoutesSpec.ts +++ b/packages/portal/backend/test/server/AdminRoutesSpec.ts @@ -611,6 +611,46 @@ describe('Admin Routes', function () { expect(person.studentNumber).to.equal(newPerson.studentNumber); // should be the same }); + it('Should NOT be able to update a classlist if NOT on a 143.103.*.* IP', async function() { + let response = null; + let body: Payload; + const url = '/portal/classlist'; + try { + response = await request(app).put(url) + .set('x-forwarded-for', '152.99.5.99') + .set('Host', 'www.google.ca'); + body = response.body; + } catch (err) { + Log.test('ERROR: ' + err); + } + + expect(body).to.haveOwnProperty('failure'); + }); + + it('Should be able to update a classlist on restricted IP', async function() { + + if (Test.isCI() === false) { + // skip locally; requires credentials devs shouldn't have (but are encrypted for CI) + Log.warn("Skipping AdminRouteSpec classlist IP test on dev machine"); + return; + } + + let response = null; + let body: Payload; + const url = '/portal/classlist'; + try { + response = await request(app).put(url) + .set('test-include-xfwd', '') + .set('x-forwarded-for', '142.103.5.99'); + body = response.body; + } catch (err) { + Log.test('ERROR: ' + err); + } + expect(body).to.haveOwnProperty('success'); + expect(body.success).to.haveOwnProperty('message'); + expect(body.success.message).to.contain('Classlist upload successful'); + }); + it('Should be able to upload a new grades', async function () { let response = null; @@ -1300,6 +1340,12 @@ describe('Admin Routes', function () { }); it('Should be able to update a classlist if authorized as admin', async function () { + if (Test.isCI() === false) { + // skip locally; requires credentials devs shouldn't have (but are encrypted for CI) + Log.warn("Skipping AdminRouteSpec classlist update test on dev machine"); + return; + } + let response = null; let body: Payload; const url = '/portal/admin/classlist'; diff --git a/packages/portal/backend/test/server/GeneralRoutesSpec.ts b/packages/portal/backend/test/server/GeneralRoutesSpec.ts index 2184bf1e8..f58519b35 100644 --- a/packages/portal/backend/test/server/GeneralRoutesSpec.ts +++ b/packages/portal/backend/test/server/GeneralRoutesSpec.ts @@ -120,39 +120,6 @@ describe('General Routes', function() { expect(body.success.githubId).to.equal(Test.USER1.github); }); - it('Should NOT be able to update a classlist if NOT on a 143.103.*.* IP', async function() { - let response = null; - let body: Payload; - const url = '/portal/classlist'; - try { - response = await request(app).put(url) - .set('x-forwarded-for', '152.99.5.99') - .set('Host', 'www.google.ca'); - body = response.body; - } catch (err) { - Log.test('ERROR: ' + err); - } - - expect(body).to.haveOwnProperty('failure'); - }); - - it('Should be able to update a classlist on restricted IP', async function() { - let response = null; - let body: Payload; - const url = '/portal/classlist'; - try { - response = await request(app).put(url) - .set('test-include-xfwd', '') - .set('x-forwarded-for', '142.103.5.99'); - body = response.body; - } catch (err) { - Log.test('ERROR: ' + err); - } - expect(body).to.haveOwnProperty('success'); - expect(body.success).to.haveOwnProperty('message'); - expect(body.success.message).to.contain('Classlist upload successful'); - }); - it('Should not be able to get a person without the right token.', async function() { const dc: DatabaseController = DatabaseController.getInstance(); From 7283dafe7bba76aa64b43eb8443a81d32b5d5858 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Mon, 19 Sep 2022 15:18:56 -0700 Subject: [PATCH 008/104] More clearly remove PR code that no longer works. --- .../src/controllers/GitHubController.ts | 107 +++++++++--------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/packages/portal/backend/src/controllers/GitHubController.ts b/packages/portal/backend/src/controllers/GitHubController.ts index fd22ecfac..f069bd0af 100644 --- a/packages/portal/backend/src/controllers/GitHubController.ts +++ b/packages/portal/backend/src/controllers/GitHubController.ts @@ -1,6 +1,3 @@ -import * as http from "http"; -import fetch, {RequestInit} from "node-fetch"; - import Config, {ConfigKey} from "../../../../common/Config"; import Log from "../../../../common/Log"; import Util from "../../../../common/Util"; @@ -423,58 +420,60 @@ export class GitHubController implements IGitHubController { */ public async createPullRequest(repo: Repository, prName: string, dryrun: boolean = false, root: boolean = false): Promise { Log.info(`GitHubController::createPullRequest(..) - Repo: (${repo.id}) start`); - // if (repo.cloneURL === null || repo.cloneURL === undefined) { - // Log.error(`GitHubController::createPullRequest(..) - ${repo.id} didn't have a valid cloneURL associated with it.`); - // return false; + throw new Error("Not implemented"); // code below used to work but depended on service that no longer exists + // // if (repo.cloneURL === null || repo.cloneURL === undefined) { + // // Log.error(`GitHubController::createPullRequest(..) - ${repo.id} didn't have a valid cloneURL associated with it.`); + // // return false; + // // } + // + // const baseUrl: string = Config.getInstance().getProp(ConfigKey.patchToolUrl); + // const patchUrl: string = `${baseUrl}/autopatch`; + // const updateUrl: string = `${baseUrl}/update`; + // const qs: string = Util.getQueryStr({ + // patch_id: prName, github_url: `${repo.URL}.git`, dryrun: String(dryrun), from_beginning: String(root) + // }); + // + // const options: RequestInit = { + // method: 'POST', + // agent: new http.Agent() + // }; + // + // let result; + // + // try { + // await fetch(patchUrl + qs, options); + // Log.info("GitHubController::createPullRequest(..) - Patch applied successfully"); + // return true; + // } catch (err) { + // result = err; + // } + // + // switch (result.statusCode) { + // case 424: + // Log.info(`GitHubController::createPullRequest(..) - ${prName} wasn't found by the patchtool. Updating patches.`); + // try { + // await fetch(updateUrl, options); + // Log.info(`GitHubController::createPullRequest(..) - Patches updated successfully. Retrying.`); + // await fetch(patchUrl + qs, {...options}); + // Log.info("GitHubController::createPullRequest(..) - Patch applied successfully on second attempt"); + // return true; + // } catch (err) { + // Log.error("GitHubController::createPullRequest(..) - Patch failed on second attempt. "+ + // "Message from patchtool server:" + result.message); + // return false; + // } + // case 500: + // Log.error( + // `GitHubController::createPullRequest(..) - patchtool internal error. " + + // "Message from patchtool server: ${result.message}` + // ); + // return false; + // default: + // Log.error( + // `GitHubController::createPullRequest(..) - Wasn't able to make a connection to patchtool. Error: ${result.message}` + // ); + // return false; // } - - const baseUrl: string = Config.getInstance().getProp(ConfigKey.patchToolUrl); - const patchUrl: string = `${baseUrl}/autopatch`; - const updateUrl: string = `${baseUrl}/update`; - const qs: string = Util.getQueryStr({ - patch_id: prName, github_url: `${repo.URL}.git`, dryrun: String(dryrun), from_beginning: String(root) - }); - - const options: RequestInit = { - method: 'POST', - agent: new http.Agent() - }; - - let result; - - try { - await fetch(patchUrl + qs, options); - Log.info("GitHubController::createPullRequest(..) - Patch applied successfully"); - return true; - } catch (err) { - result = err; - } - - switch (result.statusCode) { - case 424: - Log.info(`GitHubController::createPullRequest(..) - ${prName} wasn't found by the patchtool. Updating patches.`); - try { - await fetch(updateUrl, options); - Log.info(`GitHubController::createPullRequest(..) - Patches updated successfully. Retrying.`); - await fetch(patchUrl + qs, {...options}); - Log.info("GitHubController::createPullRequest(..) - Patch applied successfully on second attempt"); - return true; - } catch (err) { - Log.error("GitHubController::createPullRequest(..) - Patch failed on second attempt. Message from patchtool server:" + - result.message); - return false; - } - case 500: - Log.error( - `GitHubController::createPullRequest(..) - patchtool internal error. Message from patchtool server: ${result.message}` - ); - return false; - default: - Log.error( - `GitHubController::createPullRequest(..) - Wasn't able to make a connection to patchtool. Error: ${result.message}` - ); - return false; - } } /** From 21ac5aea8e04048258a04c06368d46466e8956c4 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Mon, 19 Sep 2022 15:32:36 -0700 Subject: [PATCH 009/104] prep for queue improvements --- packages/autotest/src/autotest/AutoTest.ts | 56 +++++++++++----------- packages/autotest/src/autotest/Queue.ts | 28 +++++------ packages/autotest/test/QueueSpec.ts | 5 +- 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/packages/autotest/src/autotest/AutoTest.ts b/packages/autotest/src/autotest/AutoTest.ts index 842d47732..90a2e42a3 100644 --- a/packages/autotest/src/autotest/AutoTest.ts +++ b/packages/autotest/src/autotest/AutoTest.ts @@ -15,7 +15,7 @@ export interface IAutoTest { /** * Adds a new job to be processed by the standard queue. * - * @param {IContainerInput} element + * @param {ContainerInput} element */ addToStandardQueue(element: ContainerInput): void; @@ -85,7 +85,7 @@ export abstract class AutoTest implements IAutoTest { return; } - public removeFromScheduleQueue(keys: Array<{key: string, value: string}>): ContainerInput | null { + public removeFromScheduleQueue(keys: Array<{ key: string, value: string }>): ContainerInput | null { Log.info("AutoTest::removeFromScheduleQueue(..) - start"); try { return this.scheduleQueue.removeGivenKeys(keys); @@ -112,7 +112,7 @@ export abstract class AutoTest implements IAutoTest { let updated = false; const that = this; - const schedule = function(queue: Queue): boolean { + const schedule = function (queue: Queue): boolean { const info: ContainerInput = queue.scheduleNext(); Log.info("AutoTest::tick(..) - starting job on: " + queue.getName() + "; deliv: " + info.delivId + '; repo: ' + info.target.repoId + '; SHA: ' + info.target.commitSHA); @@ -133,14 +133,14 @@ export abstract class AutoTest implements IAutoTest { return true; }; - const tickQueue = function(queue: Queue): boolean { + const tickQueue = function (queue: Queue): boolean { if (queue.length() > 0 && queue.hasCapacity() === true) { return schedule(queue); } return false; }; - const promoteQueue = function(fromQueue: Queue, toQueue: Queue): boolean { + const promoteQueue = function (fromQueue: Queue, toQueue: Queue): boolean { if (fromQueue.length() > 0 && toQueue.hasCapacity()) { Log.info("AutoTest::tick(..) - promoting: " + fromQueue.getName() + " -> " + toQueue.getName()); const info: ContainerInput = fromQueue.pop(); @@ -181,10 +181,10 @@ export abstract class AutoTest implements IAutoTest { "regression - #wait: " + this.regressionQueue.length() + ", #run: " + this.regressionQueue.numRunning() + "."); } - this.persistQueues().then(function(success: boolean) { - Log.trace("[PTEST] AutoTest::tick() - persist complete: " + success); - }).catch(function(err) { - Log.error("[PTEST] AutoTest::tick() - persist queue ERROR: " + err.message); + this.persistQueues().then(function (success: boolean) { + Log.trace("AutoTest::tick() - persist complete: " + success); + }).catch(function (err) { + Log.error("AutoTest::tick() - persist queue ERROR: " + err.message); }); } catch (err) { Log.error("AutoTest::tick() - ERROR: " + err.message); @@ -208,34 +208,34 @@ export abstract class AutoTest implements IAutoTest { } private async persistQueues(): Promise { - Log.trace("[PTEST] AutoTest::persistQueues() - start"); + Log.trace("AutoTest::persistQueues() - start"); try { const start = Date.now(); const writing = [ - this.standardQueue.persist(), - this.regressionQueue.persist(), - this.expressQueue.persist(), - this.scheduleQueue.persist() + this.standardQueue.persist(), // await in Promise.all + this.regressionQueue.persist(), // await in Promise.all + this.expressQueue.persist(), // await in Promise.all + this.scheduleQueue.persist() // await in Promise.all ]; await Promise.all(writing); - Log.trace("[PTEST] AutoTest::persistQueues() - done; took: " + Util.took(start)); + Log.trace("AutoTest::persistQueues() - done; took: " + Util.took(start)); return true; } catch (err) { - Log.error("[PTEST] AutoTest::persistQueues() - ERROR: " + err.message); + Log.error("AutoTest::persistQueues() - ERROR: " + err.message); } return false; } private loadQueues() { try { - Log.info("[PTEST] AutoTest::loadQueues() - start"); // just warn for now; this is really just for testing + Log.info("AutoTest::loadQueues() - start"); // just warn for now; this is really just for testing this.standardQueue.load(); this.regressionQueue.load(); this.expressQueue.load(); this.scheduleQueue.load(); - Log.info("[PTEST] AutoTest::loadQueues() - done; queues loaded"); + Log.info("AutoTest::loadQueues() - done; queues loaded"); } catch (err) { - Log.error("[PTEST] AutoTest::loadQueues() - ERROR: " + err.message); + Log.error("AutoTest::loadQueues() - ERROR: " + err.message); } this.tick(); } @@ -248,7 +248,7 @@ export abstract class AutoTest implements IAutoTest { * If subclasses do not want to do anything, they can just `return Promise.resolve();` * in their implementation. * - * @param {IAutoTestResult} data + * @param {AutoTestResult} data * @returns {Promise} */ protected abstract processExecution(data: AutoTestResult): Promise; @@ -308,10 +308,10 @@ export abstract class AutoTest implements IAutoTest { * Promotes a job to the express queue if it will help it to complete faster. * * This seems more complicated than it should because we want to recognize being - * next in line on an non-express queue may be faster than last in line after being + * next in line on a non-express queue may be faster than last in line after being * promoted to the express queue. * - * @param {ICommentEvent} info + * @param {CommitTarget} info */ protected promoteIfNeeded(info: CommitTarget): void { try { @@ -441,15 +441,15 @@ export abstract class AutoTest implements IAutoTest { const org = Config.getInstance().getProp(ConfigKey.org); const repoId = input.target.repoId; gradePayload = { - delivId: input.delivId, + delivId: input.delivId, repoId, - repoURL: `${githubHost}/${org}/${repoId}`, + repoURL: `${githubHost}/${org}/${repoId}`, score, - urlName: repoId, - URL: input.target.commitURL, - comment: '', + urlName: repoId, + URL: input.target.commitURL, + comment: '', timestamp: input.target.timestamp, - custom: {} + custom: {} }; } catch (err) { Log.error("AutoTest::handleTick(..) - ERROR in execution for SHA: " + input.target.commitSHA + "; ERROR: " + err); diff --git a/packages/autotest/src/autotest/Queue.ts b/packages/autotest/src/autotest/Queue.ts index 6169c86b9..e84dbb785 100644 --- a/packages/autotest/src/autotest/Queue.ts +++ b/packages/autotest/src/autotest/Queue.ts @@ -13,7 +13,7 @@ export class Queue { private readonly persistDir: string; constructor(name: string, numSlots: number) { - Log.info("[PTEST] Queue::( " + name + ", " + numSlots + " )"); + Log.info("Queue::( " + name + ", " + numSlots + " )"); this.name = name; this.numSlots = numSlots; @@ -34,7 +34,7 @@ export class Queue { * * returns the length of the array after the push. * - * @param {IContainerInput} info + * @param {ContainerInput} info * @returns {number} */ public push(info: ContainerInput): number { @@ -44,7 +44,7 @@ export class Queue { /** * Forces an item on the front of the queue. * - * @param {IContainerInput} info + * @param {ContainerInput} info * @returns {number} */ public pushFirst(info: ContainerInput): number { @@ -54,7 +54,7 @@ export class Queue { /** * Returns the first element from the queue. * - * @returns {IContainerInput | null} + * @returns {ContainerInput | null} */ public pop(): ContainerInput | null { if (this.data.length > 0) { @@ -74,7 +74,7 @@ export class Queue { * Removes an item from the queue; * * @param {string} commitURL - * @returns {IContainerInput | null} + * @returns {ContainerInput | null} */ public remove(commitURL: string): ContainerInput | null { // for (let i = 0; i < this.data.length; i++) { @@ -95,7 +95,7 @@ export class Queue { * @returns {ContainerInput | null} * @param keys */ - public removeGivenKeys(keys: Array<{key: string, value: any}>): ContainerInput | null { + public removeGivenKeys(keys: Array<{ key: string, value: any }>): ContainerInput | null { for (let i = this.data.length - 1; i >= 0; i--) { const info: ContainerInput = this.data[i]; if (keys.every((kv) => (info.target as any)[kv.key] === kv.value)) { @@ -189,7 +189,7 @@ export class Queue { public async persist(): Promise { try { - Log.trace("[PTEST] Queue::persist() - saving: " + this.name + " to: " + this.persistDir + + Log.trace("Queue::persist() - saving: " + this.name + " to: " + this.persistDir + " # slots: " + this.slots.length + "; # data: " + this.data.length); // push current elements back onto the front of the stack @@ -198,7 +198,7 @@ export class Queue { return true; } catch (err) { - Log.error("[PTEST] Queue::persist() - ERROR: " + err.message); + Log.error("Queue::persist() - ERROR: " + err.message); return false; } } @@ -207,27 +207,27 @@ export class Queue { try { // this happens so infrequently, we will do it synchronously const store = fs.readJSONSync(this.persistDir); - Log.info("[PTEST] Queue::load() - rehydrating: " + this.name + " from: " + this.persistDir); - Log.info("[PTEST] Queue::load() - rehydrating: " + + Log.info("Queue::load() - rehydrating: " + this.name + " from: " + this.persistDir); + Log.info("Queue::load() - rehydrating: " + this.name + "; # slots: " + store.slots.length + "; # data: " + store.data.length); // put executions that were running but not done on the front of the queue for (const slot of store.slots) { - Log.info("[PTEST] Queue::load() - queue: " + this.name + + Log.info("Queue::load() - queue: " + this.name + "; add executing to HEAD: " + slot.target.commitURL); this.pushFirst(slot); // add to the head of the queued list (if we are restarting this will always be true anyways) } // push all other planned executions to the end of the queue for (const data of store.data) { - Log.info("[PTEST] Queue::load() - queue: " + this.name + + Log.info("Queue::load() - queue: " + this.name + "; add queued to TAIL: " + data.target.commitURL); this.push(data); // add to the head of the queued list (if we are restarting this will always be true anyways) } - Log.info("[PTEST] Queue::load() - rehydrating: " + this.name + " - done"); + Log.info("Queue::load() - rehydrating: " + this.name + " - done"); } catch (err) { // if anything happens just don't add to the queue - Log.error("[PTEST] Queue::load() - ERROR rehydrating queue: " + err.message); + Log.error("Queue::load() - ERROR rehydrating queue: " + err.message); } } } diff --git a/packages/autotest/test/QueueSpec.ts b/packages/autotest/test/QueueSpec.ts index e61a286dc..df8fd5433 100644 --- a/packages/autotest/test/QueueSpec.ts +++ b/packages/autotest/test/QueueSpec.ts @@ -2,7 +2,6 @@ import {expect} from "chai"; import "mocha"; import Log from "../../common/Log"; -import {Test} from "../../common/TestHarness"; import {Queue} from "../src/autotest/Queue"; // const loadFirst = require('./GlobalSpec'); import "./GlobalSpec"; @@ -12,12 +11,12 @@ describe("Queue", () => { let q: Queue; - before(function() { + before(function () { Log.test("QueueSpec::before"); q = new Queue('test', 1); }); - after(function() { + after(function () { Log.test("QueueSpec::after"); q = new Queue('test', 1); }); From bc117293963719121e411d147b87f86388c02c22 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 08:23:25 -0700 Subject: [PATCH 010/104] Major rewrite of AutoTest queues. Much higher priorty on the express queue now (will execute in standard and regression slots, if express jobs are waiting). Initial support for DOS support: if more than 4 jobs are present from a repo, jobs are moved onto the regression queue, which is handled last. More should be done here: admin jobs shouldn't count against this total, regression jobs should be ordered in a way that pushes the heavy executors to the bottom. --- packages/autotest/src/autotest/AutoTest.ts | 534 ++++++++++++------ packages/autotest/src/autotest/DataStore.ts | 2 +- packages/autotest/src/autotest/Queue.ts | 171 ++++-- .../autotest/src/github/GitHubAutoTest.ts | 251 +++----- packages/autotest/src/github/GitHubUtil.ts | 28 +- packages/autotest/test/ClassPortalSpec.ts | 1 + packages/autotest/test/GitHubAutoTestSpec.ts | 30 +- packages/autotest/test/GitHubEventSpec.ts | 11 +- packages/autotest/test/QueueSpec.ts | 10 +- packages/autotest/test/TestData.ts | 6 + packages/common/TestHarness.ts | 1 + packages/common/types/ContainerTypes.ts | 3 +- tslint.json | 1 + 13 files changed, 639 insertions(+), 410 deletions(-) diff --git a/packages/autotest/src/autotest/AutoTest.ts b/packages/autotest/src/autotest/AutoTest.ts index 90a2e42a3..e83f87480 100644 --- a/packages/autotest/src/autotest/AutoTest.ts +++ b/packages/autotest/src/autotest/AutoTest.ts @@ -2,7 +2,7 @@ import * as Docker from "dockerode"; import Config, {ConfigKey} from "../../../common/Config"; import Log from "../../../common/Log"; import {AutoTestResult} from "../../../common/types/AutoTestTypes"; -import {CommitTarget, ContainerInput} from "../../../common/types/ContainerTypes"; +import {ContainerInput} from "../../../common/types/ContainerTypes"; import {AutoTestGradeTransport} from "../../../common/types/PortalTypes"; import Util from "../../../common/Util"; import {IClassPortal} from "./ClassPortal"; @@ -12,6 +12,14 @@ import {MockGradingJob} from "./mocks/MockGradingJob"; import {Queue} from "./Queue"; export interface IAutoTest { + + /** + * Adds a new job to be processed by the express queue. + * + * @param {ContainerInput} element + */ + addToExpressQueue(element: ContainerInput): void; + /** * Adds a new job to be processed by the standard queue. * @@ -19,29 +27,93 @@ export interface IAutoTest { */ addToStandardQueue(element: ContainerInput): void; - // NOTE: add this when we support regression queues - // addToRegressionQueue(element: IContainerInput): void; + /** + * Adds a new job to be processed by the regression queue. + * + * @param {ContainerInput} element + */ + addToRegressionQueue(element: ContainerInput): void; /** * Updates the internal clock of the handler. This might or might not do anything. * - * But if there are execution slots available and the queue has elements it should - * start jobs processing. + * But if there are execution slots on any queue available, and any queue has jobs waiting, + * a waiting job should start processing on the available slot. */ tick(): void; } +/** + * Handles the scheduling and prioritization of AutoTest jobs. + * + * In general, queueing looks like this: + * + * 1) Express jobs are always handled if there is a job waiting + * and an execution slot available on any other queue. + * + * 2) Standard jobs run when no express jobs are waiting. If too + * many jobs are added to this queue from a single user the + * jobs are demoted to the regression queue. + * + * 3) Regression jobs are run when no express or standard jobs + * are waiting. + * + * NOTE: the schedule queue is its own thing and does not interact + * with any of these other queues except that it puts jobs into + * the express queue when they're ready. + * + */ export abstract class AutoTest implements IAutoTest { protected readonly dataStore: IDataStore; protected readonly classPortal: IClassPortal = null; protected readonly docker: Docker; - private regressionQueue = new Queue('regression', 1); - private standardQueue = new Queue('standard', 2); + /** + * Express queue. If this queue has jobs in it, no matter what is + * going on in the other queues, these should be handled first. + * + * @private {Queue} + */ private expressQueue = new Queue('express', 2); - private scheduleQueue = new Queue('schedule', 0); - // noinspection TypeScriptAbstractClassConstructorCanBeMadeProtected + /** + * Standard jobs. Always execute after Express jobs, but also always + * before any regression jobs. + * + * @private {Queue} + */ + private standardQueue = new Queue('standard', 2); + + /** + * Regression jobs. These will happen whenever they can. Repos + * that push too rapidly will have their jobs demoted to the + * regression queue. + * + * @private {Queue} + */ + private regressionQueue = new Queue('regression', 1); + + /** + * The maximum number of jobs a single user can have on the standard queue + * before it will schedule on the regression queue instead. This is to + * prevent DOS attacks because a single user could submit an unbounded number + * of requests preventing others from being graded. + * + * @private + */ + private readonly MAX_STANDARD_JOBS: number = 4; + + /** + * The maximum number of jobs a single user can have on the regression queue + * before we refrain from scheduling them at all. + * + * NOTE: not currently used. + * + * @private + */ + // noinspection JSUnusedLocalSymbols + // private readonly MAX_JOBS: number = 100; + constructor(dataStore: IDataStore, classPortal: IClassPortal, docker: Docker) { Log.info("AutoTest:: - starting AutoTest"); this.dataStore = dataStore; @@ -56,10 +128,73 @@ export abstract class AutoTest implements IAutoTest { }, 1000 * 60 * 5); } + /** + * Adds a job to the express queue. A user can only ask for a single job to go on + * the express queue at a time. If the job is already executing on any other queue + * it will not be added. + * + * @param input + */ + public addToExpressQueue(input: ContainerInput): void { + Log.info("AutoTest::addToExpressQueue(..) - start; commit: " + input.target.commitSHA); + try { + + if (this.isCommitExecuting(input)) { + Log.info("AutoTest::addToExpressQueue(..) - not added; commit already executing"); + return; + } + + if (this.expressQueue.hasWaitingJobForRequester(input) === false) { + // add to express queue + this.expressQueue.push(input); + + // if job is on any other queue, remove it + this.standardQueue.remove(input); + this.regressionQueue.remove(input); + } else { + Log.info("AutoTest::addToExpressQueue(..) - user: " + input.target.personId + + " already has job on express queue; adding: " + + input.target.commitSHA + " to standard queue"); + + // express queue already has a job for this user, move to standard + this.addToStandardQueue(input); + } + } catch (err) { + Log.error("AutoTest::addToExpressQueue(..) - ERROR: " + err); + } + } + public addToStandardQueue(input: ContainerInput): void { Log.info("AutoTest::addToStandardQueue(..) - start; commit: " + input.target.commitSHA); try { - this.standardQueue.push(input); + + if (this.isCommitExecuting(input)) { + Log.info("AutoTest::addToStandardQueue(..) - not added; commit already executing"); + return; + } + + // only add job if it is not already on express + if (this.expressQueue.indexOf(input) < 0) { + + const standardJobCount = this.standardQueue.numberJobsForRepo(input); + const regressionJobCount = this.regressionQueue.numberJobsForRepo(input); + if (standardJobCount < this.MAX_STANDARD_JOBS) { + this.standardQueue.push(input); + + // if job is on any other queue, remove it + this.regressionQueue.remove(input); + } else { + Log.warn("AutoTest::addToStandardQueue(..) - user: " + + input.target.personId + "; has #" + standardJobCount + + " standard jobs queued and #" + regressionJobCount + + " regression jobs queued"); + this.addToRegressionQueue(input); + } + + } else { + Log.info("AutoTest::addToStandardQueue(..) - skipped; " + + "job already on express queue; SHA: " + input.target.commitSHA); + } } catch (err) { Log.error("AutoTest::addToStandardQueue(..) - ERROR: " + err); } @@ -68,106 +203,138 @@ export abstract class AutoTest implements IAutoTest { public addToRegressionQueue(input: ContainerInput): void { Log.info("AutoTest::addToRegressionQueue(..) - start; commit: " + input.target.commitSHA); try { - this.regressionQueue.push(input); - } catch (err) { - Log.error("AutoTest::addToRegressionQueue(..) - ERROR: " + err); - } - } - public addToScheduleQueue(input: ContainerInput): void { - Log.info("AutoTest::addToScheduleQueue(..) - start; commit: " + input.target.commitSHA); - try { - this.scheduleQueue.push(input); - this.scheduleQueue.sort("timestamp"); - } catch (err) { - Log.error("AutoTest::addToScheduleQueue(..) - ERROR: " + err); - } - return; - } + if (this.isCommitExecuting(input)) { + Log.info("AutoTest::addToRegressionQueue(..) - not added; commit already executing"); + return; + } - public removeFromScheduleQueue(keys: Array<{ key: string, value: string }>): ContainerInput | null { - Log.info("AutoTest::removeFromScheduleQueue(..) - start"); - try { - return this.scheduleQueue.removeGivenKeys(keys); + // add to the regression queue if it is not already on express or standard + if (this.expressQueue.indexOf(input) < 0 && this.standardQueue.indexOf(input) < 0) { + this.regressionQueue.push(input); + } else { + Log.info("AutoTest::addToRegressionQueue(..) - skipped; " + + "job already on standard or express queue; SHA: " + input.target.commitSHA); + } } catch (err) { - Log.error("AutoTest::removeFromScheduleQueue(..) - ERROR: " + err); + Log.error("AutoTest::addToRegressionQueue(..) - ERROR: " + err); } - return null; } - public tick() { + /** + * Advance the queues. Does nothing if all execution slots are full. + */ + public tick(): void { try { Log.info("AutoTest::tick(..) - start; " + - "standard - #wait: " + this.standardQueue.length() + ", #run: " + this.standardQueue.numRunning() + "; " + "express - #wait: " + this.expressQueue.length() + ", #run: " + this.expressQueue.numRunning() + "; " + - "regression - #wait: " + this.regressionQueue.length() + ", #run: " + this.regressionQueue.numRunning() + "; " + - "schedule - #wait: " + this.scheduleQueue.length() + "."); - - // Move scheduled items that are not eligible to run into the standard queue - this.updateScheduleQueue(); - - // Log.info("AutoTest::tick(..) - moved jobs from the schedule to the standard queue; " + - // "standard - #wait: " + this.standardQueue.length() + "."); + "standard - #wait: " + this.standardQueue.length() + ", #run: " + this.standardQueue.numRunning() + "; " + + "regression - #wait: " + this.regressionQueue.length() + ", #run: " + this.regressionQueue.numRunning() + "."); - let updated = false; + // let updated = false; const that = this; - const schedule = function (queue: Queue): boolean { - const info: ContainerInput = queue.scheduleNext(); - Log.info("AutoTest::tick(..) - starting job on: " + queue.getName() + "; deliv: " + - info.delivId + '; repo: ' + info.target.repoId + '; SHA: ' + info.target.commitSHA); + const tickQueue = function (queue: Queue): void { + if (queue.length() > 0 && queue.hasCapacity() === true) { + const info: ContainerInput = queue.scheduleNext(); + Log.info("AutoTest::tick::tickQueue(..) - starting job on: " + queue.getName() + "; deliv: " + + info.delivId + '; repo: ' + info.target.repoId + '; SHA: ' + info.target.commitSHA); + + let gradingJob: GradingJob; + // Use mocked GradingJob if testing; EMPTY and POSTBACK used by test environment + if (info.target.postbackURL === "EMPTY" || info.target.postbackURL === "POSTBACK") { + Log.warn("AutoTest::tick::tickQueue(..) - Running grading job in test mode."); + gradingJob = new MockGradingJob(info); + } else { + gradingJob = new GradingJob(info); + } - let gradingJob: GradingJob; - // Use mocked GradingJob if testing; EMPTY and POSTBACK used by test environment - if (info.target.postbackURL === "EMPTY" || info.target.postbackURL === "POSTBACK") { - Log.warn("AutoTest::tick(..) - Running grading job in test mode."); - gradingJob = new MockGradingJob(info); + // noinspection ES6MissingAwait + // noinspection JSIgnoredPromiseFromCall + // tslint:disable-next-line + that.handleTick(gradingJob); // NOTE: not awaiting on purpose (let it finish in the background)! } else { - gradingJob = new GradingJob(info); + // no cap to tick (shouldn't happen) + Log.trace("AutoTest::tick::tickQueue(..) - no capacity to tick"); } - - // noinspection JSIgnoredPromiseFromCall - // tslint:disable-next-line - that.handleTick(gradingJob); // NOTE: not awaiting on purpose (let it finish in the background)! - updated = true; - return true; }; - const tickQueue = function (queue: Queue): boolean { - if (queue.length() > 0 && queue.hasCapacity() === true) { - return schedule(queue); + /** + * Moves a job from one queue to another. + * + * @param input + * @param sourceQueue + * @param destQueue + * @param onFront whether the job should be put at the front (true) or back (false) of the queue. + */ + const switchQueues = function (input: ContainerInput, sourceQueue: Queue, destQueue: Queue, onFront: boolean) { + Log.info("AutoTest::tick::switchQueues(..) - start; source: " + sourceQueue.getName() + + "-> dest: " + destQueue.getName() + "; for SHA: " + input.target.commitSHA); + + if (that.isCommitExecuting(input)) { + Log.info("AutoTest::tick::switchQueues(..) - skipped; commit already executing"); + return; + } + + const onSourceQueue = sourceQueue.indexOf(input) >= 0; + const onDestQueue = destQueue.indexOf(input) >= 0; + + if (onDestQueue === true) { + // already on dest queue + Log.warn("AutoTest::tick::switchQueues(..) - already on dest queue: " + input.target.commitSHA); + return; + } + + if (onSourceQueue === false) { + // not on source to switch + Log.warn("AutoTest::tick::switchQueues(..) - not on source queue: " + input.target.commitSHA); + return; } - return false; - }; - const promoteQueue = function (fromQueue: Queue, toQueue: Queue): boolean { - if (fromQueue.length() > 0 && toQueue.hasCapacity()) { - Log.info("AutoTest::tick(..) - promoting: " + fromQueue.getName() + " -> " + toQueue.getName()); - const info: ContainerInput = fromQueue.pop(); - toQueue.pushFirst(info); - return schedule(toQueue); + // swap queues + Log.trace("AutoTest::tick::switchQueues(..) - switching: " + input.target.commitSHA); + sourceQueue.remove(input); + if (onFront === true) { + destQueue.pushFirst(input); // put on the front of the next queue + } else { + destQueue.push(input); // put on the front of the next queue } - return false; + Log.trace("AutoTest::tick::switchQueues(..) - switched: " + input.target.commitSHA); }; - // express first; if jobs are waiting here, make them happen - tickQueue(this.expressQueue); - // express -> regression; if express jobs are waiting, override regression queue - promoteQueue(this.expressQueue, this.regressionQueue); - // express -> standard; if express jobs are waiting, override standard queue - promoteQueue(this.expressQueue, this.standardQueue); - - // standard second; if slots are available after express promotions, schedule these - tickQueue(this.standardQueue); - // standard -> regression; if regression has space, run the standard queue here - promoteQueue(this.standardQueue, this.regressionQueue); - - // regression; only schedule if others have no waiting jobs - tickQueue(this.regressionQueue); - // regression -> standard; if standard has space (after checking express and standard), run the regression queue here - promoteQueue(this.regressionQueue, this.standardQueue); - // regression -> express; NEVER do this; this is intentionally disabled so express is always available - // promoteQueue(--- BAD IDEA this.regressionQueue, this.expressQueue BAD IDEA ---); + // fill all express execution slots with express jobs + while (this.expressQueue.hasCapacity() && this.expressQueue.hasWaitingJobs()) { + tickQueue(this.expressQueue); + } + + // fill all standard execution slots with express jobs + while (this.standardQueue.hasCapacity() && this.expressQueue.hasWaitingJobs()) { + // move express job to standard slot + switchQueues(this.expressQueue.peek(), this.expressQueue, this.standardQueue, true); + tickQueue(this.standardQueue); + } + + // fill all regression slots with express jobs + while (this.regressionQueue.hasCapacity() && this.expressQueue.hasWaitingJobs()) { + switchQueues(this.expressQueue.peek(), this.expressQueue, this.regressionQueue, true); + tickQueue(this.regressionQueue); + } + + // fill standard slots with standard jobs + while (this.standardQueue.hasCapacity() && this.standardQueue.hasWaitingJobs()) { + tickQueue(this.standardQueue); + } + + // fill regression slots with standard jobs + while (this.regressionQueue.hasCapacity() && this.standardQueue.hasWaitingJobs()) { + switchQueues(this.standardQueue.peek(), this.standardQueue, this.regressionQueue, true); + tickQueue(this.regressionQueue); + } + + // finally, run the regression queue with any of its jobs that are waiting + while (this.regressionQueue.hasCapacity() && this.regressionQueue.hasWaitingJobs()) { + tickQueue(this.regressionQueue); + } if (this.standardQueue.length() === 0 && this.standardQueue.numRunning() === 0 && this.expressQueue.length() === 0 && this.expressQueue.numRunning() === 0 && @@ -176,8 +343,8 @@ export abstract class AutoTest implements IAutoTest { } else { // Log.info("AutoTest::tick(..) - done - execution slots busy; no new jobs started"); Log.info("AutoTest::tick(..) - done: " + - "standard - #wait: " + this.standardQueue.length() + ", #run: " + this.standardQueue.numRunning() + "; " + "express - #wait: " + this.expressQueue.length() + ", #run: " + this.expressQueue.numRunning() + "; " + + "standard - #wait: " + this.standardQueue.length() + ", #run: " + this.standardQueue.numRunning() + "; " + "regression - #wait: " + this.regressionQueue.length() + ", #run: " + this.regressionQueue.numRunning() + "."); } @@ -191,31 +358,104 @@ export abstract class AutoTest implements IAutoTest { } } - private updateScheduleQueue(): void { - Log.trace("AutoTest::updateScheduleQueue() - updating the schedule queue"); - let scheduleQueueInput = this.scheduleQueue.peek(); - const compareTime = Date.now(); - while (scheduleQueueInput !== null && scheduleQueueInput.target.timestamp < compareTime) { - Log.trace("AutoTest::updateScheduleQueue() - Adding to the standard queue from scheduled"); - this.addToStandardQueue(this.scheduleQueue.pop()); - scheduleQueueInput = this.scheduleQueue.peek(); - // TODO create handleScheduleQueuePop - // Implemented by child class - // GitHubAutoTest will just call - // handleCommentStudent(info,await this.classPortal.getResult(info.delivId, info.repoId, info.commitSHA)) - // after it deletes the #schdule flag - } - } + // public tick() { + // try { + // Log.info("AutoTest::tick(..) - start; " + + // "standard - #wait: " + this.standardQueue.length() + ", #run: " + this.standardQueue.numRunning() + "; " + + // "express - #wait: " + this.expressQueue.length() + ", #run: " + this.expressQueue.numRunning() + "; " + + // "regression - #wait: " + this.regressionQueue.length() + ", #run: " + this.regressionQueue.numRunning() + "."); + // + // let updated = false; + // const that = this; + // + // const schedule = function (queue: Queue): boolean { + // const info: ContainerInput = queue.scheduleNext(); + // Log.info("AutoTest::tick(..) - starting job on: " + queue.getName() + "; deliv: " + + // info.delivId + '; repo: ' + info.target.repoId + '; SHA: ' + info.target.commitSHA); + // + // let gradingJob: GradingJob; + // // Use mocked GradingJob if testing; EMPTY and POSTBACK used by test environment + // if (info.target.postbackURL === "EMPTY" || info.target.postbackURL === "POSTBACK") { + // Log.warn("AutoTest::tick(..) - Running grading job in test mode."); + // gradingJob = new MockGradingJob(info); + // } else { + // gradingJob = new GradingJob(info); + // } + // + // // noinspection JSIgnoredPromiseFromCall + // // tslint:disable-next-line + // that.handleTick(gradingJob); // NOTE: not awaiting on purpose (let it finish in the background)! + // updated = true; + // return true; + // }; + // + // const tickQueue = function (queue: Queue): boolean { + // if (queue.length() > 0 && queue.hasCapacity() === true) { + // return schedule(queue); + // } + // return false; + // }; + // + // const promoteQueue = function (fromQueue: Queue, toQueue: Queue): boolean { + // if (fromQueue.length() > 0 && toQueue.hasCapacity()) { + // Log.info("AutoTest::tick(..) - promoting: " + fromQueue.getName() + " -> " + toQueue.getName()); + // const info: ContainerInput = fromQueue.pop(); + // toQueue.pushFirst(info); + // return schedule(toQueue); + // } + // return false; + // }; + // + // // express first; if jobs are waiting here, make them happen + // tickQueue(this.expressQueue); + // // express -> regression; if express jobs are waiting, override regression queue + // promoteQueue(this.expressQueue, this.regressionQueue); + // // express -> standard; if express jobs are waiting, override standard queue + // promoteQueue(this.expressQueue, this.standardQueue); + // + // // standard second; if slots are available after express promotions, schedule these + // tickQueue(this.standardQueue); + // // standard -> regression; if regression has space, run the standard queue here + // promoteQueue(this.standardQueue, this.regressionQueue); + // + // // regression; only schedule if others have no waiting jobs + // tickQueue(this.regressionQueue); + // // regression -> standard; if standard has space (after checking express and standard), run the regression queue here + // promoteQueue(this.regressionQueue, this.standardQueue); + // // regression -> express; NEVER do this; this is intentionally disabled so express is always available + // // promoteQueue(--- BAD IDEA this.regressionQueue, this.expressQueue BAD IDEA ---); + // + // if (this.standardQueue.length() === 0 && this.standardQueue.numRunning() === 0 && + // this.expressQueue.length() === 0 && this.expressQueue.numRunning() === 0 && + // this.regressionQueue.length() === 0 && this.regressionQueue.numRunning() === 0) { + // Log.info("AutoTest::tick(..) - done: queues empty and idle; no new jobs started."); + // } else { + // // Log.info("AutoTest::tick(..) - done - execution slots busy; no new jobs started"); + // Log.info("AutoTest::tick(..) - done: " + + // "standard - #wait: " + this.standardQueue.length() + ", #run: " + this.standardQueue.numRunning() + "; " + + // "express - #wait: " + this.expressQueue.length() + ", #run: " + this.expressQueue.numRunning() + "; " + + // "regression - #wait: " + this.regressionQueue.length() + ", #run: " + this.regressionQueue.numRunning() + "."); + // } + // + // this.persistQueues().then(function (success: boolean) { + // Log.trace("AutoTest::tick() - persist complete: " + success); + // }).catch(function (err) { + // Log.error("AutoTest::tick() - persist queue ERROR: " + err.message); + // }); + // } catch (err) { + // Log.error("AutoTest::tick() - ERROR: " + err.message); + // } + // } private async persistQueues(): Promise { Log.trace("AutoTest::persistQueues() - start"); try { const start = Date.now(); + // noinspection ES6MissingAwait const writing = [ this.standardQueue.persist(), // await in Promise.all this.regressionQueue.persist(), // await in Promise.all - this.expressQueue.persist(), // await in Promise.all - this.scheduleQueue.persist() // await in Promise.all + this.expressQueue.persist() ]; await Promise.all(writing); Log.trace("AutoTest::persistQueues() - done; took: " + Util.took(start)); @@ -232,7 +472,6 @@ export abstract class AutoTest implements IAutoTest { this.standardQueue.load(); this.regressionQueue.load(); this.expressQueue.load(); - this.scheduleQueue.load(); Log.info("AutoTest::loadQueues() - done; queues loaded"); } catch (err) { Log.error("AutoTest::loadQueues() - ERROR: " + err.message); @@ -256,20 +495,20 @@ export abstract class AutoTest implements IAutoTest { /** * Returns whether the commitURL is currently executing the given deliverable. * - * @param commitURL - * @param delivId + * @param {ContainerInput} input + * @returns {boolean} true if a commit is executing on any of the queues */ - protected isCommitExecuting(commitURL: string, delivId: string): boolean { + protected isCommitExecuting(input: ContainerInput): boolean { try { - if (this.standardQueue.isCommitExecuting(commitURL, delivId) === true) { + if (this.standardQueue.isCommitExecuting(input) === true) { return true; } - if (this.expressQueue.isCommitExecuting(commitURL, delivId) === true) { + if (this.expressQueue.isCommitExecuting(input) === true) { return true; } - if (this.regressionQueue.isCommitExecuting(commitURL, delivId) === true) { + if (this.regressionQueue.isCommitExecuting(input) === true) { return true; } @@ -280,22 +519,21 @@ export abstract class AutoTest implements IAutoTest { } /** - * Checks to see of a commitURL is queued or is currently being executed + * Checks to see of a commit is queued or is currently being executed * - * @param {string} commitURL - * @param {string} delivId - * @returns {boolean} + * @param {ContainerInput} input + * @returns {boolean} true if a commit is on any queue, or is currently executing on any queue. */ - protected isOnQueue(commitURL: string, delivId: string): boolean { + protected isOnQueue(input: ContainerInput): boolean { let onQueue = false; try { - if (this.isCommitExecuting(commitURL, delivId) === true) { + if (this.isCommitExecuting(input) === true) { onQueue = true; - } else if (this.standardQueue.indexOf(commitURL) >= 0) { + } else if (this.standardQueue.indexOf(input) >= 0) { onQueue = true; - } else if (this.expressQueue.indexOf(commitURL) >= 0) { + } else if (this.expressQueue.indexOf(input) >= 0) { onQueue = true; - } else if (this.regressionQueue.indexOf(commitURL) >= 0) { + } else if (this.regressionQueue.indexOf(input) >= 0) { onQueue = true; } } catch (err) { @@ -304,58 +542,6 @@ export abstract class AutoTest implements IAutoTest { return onQueue; } - /** - * Promotes a job to the express queue if it will help it to complete faster. - * - * This seems more complicated than it should because we want to recognize being - * next in line on a non-express queue may be faster than last in line after being - * promoted to the express queue. - * - * @param {CommitTarget} info - */ - protected promoteIfNeeded(info: CommitTarget): void { - try { - Log.trace("AutoTest::promoteIfNeeded() - start"); - - if (this.isCommitExecuting(info.commitURL, info.delivId) === true) { - Log.trace("AutoTest::promoteIfNeeded() - not needed; currently executing"); - return; - } - - if (this.standardQueue.indexOf(info.commitURL) >= 0) { - // is on the standard queue - if (this.expressQueue.length() > this.standardQueue.indexOf(info.commitURL)) { - // faster to just leave it on the standard queue - } else { - // promote to the express queue - const input = this.standardQueue.remove(info.commitURL); - if (input !== null) { - Log.trace("AutoTest::promoteIfNeeded() - job moved from standard to express queue: " + info.commitSHA); - this.expressQueue.push(input); - } - } - } else if (this.regressionQueue.indexOf(info.commitURL) >= 0) { - // is on the regression queue - if (this.expressQueue.length() > this.regressionQueue.indexOf(info.commitURL)) { - // faster to just leave it on the regression queue - } else { - // promote to the express queue - const input = this.regressionQueue.remove(info.commitURL); - if (input !== null) { - Log.trace("AutoTest::promoteIfNeeded() - job moved from regression to express queue: " + info.commitSHA); - this.expressQueue.push(input); - } - } - } else { - // not an error: - // this happens if we try to promote after a job is done but before the queue is cleared - // or if it is already on the express queue - } - } catch (err) { - Log.error("AutoTest::promoteIfNeeded() - ERROR: " + err); - } - } - /** * Called when a container completes. * diff --git a/packages/autotest/src/autotest/DataStore.ts b/packages/autotest/src/autotest/DataStore.ts index e0d48832a..efd81bddd 100644 --- a/packages/autotest/src/autotest/DataStore.ts +++ b/packages/autotest/src/autotest/DataStore.ts @@ -51,7 +51,7 @@ export interface IDataStore { /** * Debugging / testing only, should not be commonly used. * - * @returns {Promise<{records: ICommitRecord[]; comments: ICommentEvent[]; pushes: IPushEvent[]; feedback: IFeedbackGiven[]}>} + * @returns {Promise<{records: AutoTestResult[]; comments: CommitTarget[]; pushes: CommitTarget[]; feedback: IFeedbackGiven[]}>} */ getAllData(): Promise<{records: AutoTestResult[], comments: CommitTarget[], pushes: CommitTarget[], feedback: IFeedbackGiven[]}>; diff --git a/packages/autotest/src/autotest/Queue.ts b/packages/autotest/src/autotest/Queue.ts index e84dbb785..7b1df4899 100644 --- a/packages/autotest/src/autotest/Queue.ts +++ b/packages/autotest/src/autotest/Queue.ts @@ -30,15 +30,30 @@ export class Queue { } /** - * Pushes on the end of the queue. + * Pushes on the end of the queue, if it is not already present. * - * returns the length of the array after the push. + * Returns the length of the array after the push. * - * @param {ContainerInput} info + * @param {ContainerInput} input * @returns {number} */ - public push(info: ContainerInput): number { - return this.data.push(info); // end of queue + public push(input: ContainerInput): number { + + if (typeof input.target.adminRequest !== "undefined" && + input.target.adminRequest !== null && + input.target.adminRequest === true) { + // put admin requests on the front of the queue + Log.info("Queue:push(..) - admin request; pushing to head of queue"); + this.pushFirst(input); + } else { + if (this.indexOf(input) < 0) { + this.data.push(input); // end of queue + } else { + Log.info("Queue:push(..) - job already on queue: " + input.target.commitURL); + } + } + + return this.data.length; } /** @@ -52,7 +67,7 @@ export class Queue { } /** - * Returns the first element from the queue. + * Removes the first element from the queue and returns it. * * @returns {ContainerInput | null} */ @@ -63,6 +78,9 @@ export class Queue { return null; } + /** + * Copies the first element from the queue but does not remove it. + */ public peek(): ContainerInput | null { if (this.data.length > 0) { return Object.assign({}, this.data[0]); @@ -71,17 +89,16 @@ export class Queue { } /** - * Removes an item from the queue; + * Removes an item from the queue. * - * @param {string} commitURL - * @returns {ContainerInput | null} + * @param {ContainerInput} info + * @returns {ContainerInput | null} returns null if no job was removed */ - public remove(commitURL: string): ContainerInput | null { - // for (let i = 0; i < this.data.length; i++) { + public remove(info: ContainerInput): ContainerInput | null { for (let i = this.data.length - 1; i >= 0; i--) { // count down instead of up so we don't miss anything after a removal - const info = this.data[i]; - if (info.target.commitURL === commitURL) { + const queued = this.data[i]; + if (queued.target.commitURL === info.target.commitURL && queued.target.delivId === info.target.delivId) { this.data.splice(i, 1); return info; } @@ -90,57 +107,105 @@ export class Queue { } /** - * Removes an item from the queue given key values to match in info.target; + * Returns the index of a given container. * - * @returns {ContainerInput | null} - * @param keys + * @param {ContainerInput} info + * @returns {number} index of the provided SHA, or -1 if not present */ - public removeGivenKeys(keys: Array<{ key: string, value: any }>): ContainerInput | null { - for (let i = this.data.length - 1; i >= 0; i--) { - const info: ContainerInput = this.data[i]; - if (keys.every((kv) => (info.target as any)[kv.key] === kv.value)) { - this.data.splice(i, 1); - return info; + public indexOf(info: ContainerInput): number { + for (let i = 0; i < this.data.length; i++) { + const queued = this.data[i]; + if (queued.target.commitURL === info.target.commitURL && queued.target.delivId === info.target.delivId) { + return i; } } - return null; + return -1; } - public sort(key: string) { - this.data.sort((a: ContainerInput, b: ContainerInput) => { - if ((a.target as any)[key] < (b.target as any)[key]) { - return -1; - } else if ((a.target as any)[key] > (b.target as any)[key]) { - return 1; - } else { - return 0; - } - }); + /** + * The number of elements waiting on the queue. + */ + public length(): number { + return this.data.length; } - public indexOf(commitURL: string): number { - for (let i = 0; i < this.data.length; i++) { - const info = this.data[i]; - if (info.target.commitURL === commitURL) { - return i; + /** + * Whether any jobs are waiting to execute. + */ + public hasWaitingJobs(): boolean { + return this.data.length > 0; + } + + /** + * Returns true if a job is already waiting for a requester on this queue. + * + * @param input + */ + public hasWaitingJobForRequester(input: ContainerInput): boolean { + for (const job of this.data) { + if (input.target.personId !== null && typeof input.target.personId !== "undefined" && + job.target.personId === input.target.personId) { + return true; } } - return -1; + for (const job of this.slots) { + if (input.target.personId !== null && typeof input.target.personId !== "undefined" && + job.target.personId === input.target.personId) { + return true; + } + } + return false; } - public length(): number { - return this.data.length; + /** + * Returns the number of queued or executing jobs for a repo. + * + * NOTE: it would be better for this to be per requester, but + * often push events do not have this info. + * + * @param input + */ + public numberJobsForRepo(input: ContainerInput): number { + let count = 0; + for (const job of this.data) { + if (input.target.repoId !== null && typeof input.target.repoId !== "undefined" && + job.target.repoId === input.target.repoId) { + count++; + } + } + for (const job of this.slots) { + if (input.target.repoId !== null && typeof input.target.repoId !== "undefined" && + job.target.repoId === input.target.repoId) { + count++; + } + } + return count; } - public isCommitExecuting(commitURL: string, delivId: string) { + /** + * Returns whether a given SHA:deliv tuple is executing on the current queue. + * + * @param {ContainerInput} input + * @returns {boolean} whether the commit/delivId tuple is executing on the current queue. + */ + public isCommitExecuting(input: ContainerInput): boolean { for (const execution of this.slots) { - if (execution.target.commitURL === commitURL && execution.delivId === delivId) { + if (execution.target.commitURL === input.target.commitURL && + execution.delivId === input.target.delivId) { return true; } } return false; } + /** + * Returns whether a given SHA:deliv tuple is executing on the current queue; + * if true, the job is also removed from its execution slot so another job + * can be started. + * + * @param commitURL + * @param delivId + */ public clearExecution(commitURL: string, delivId: string): boolean { let removed = false; for (let i = this.slots.length - 1; i >= 0; i--) { @@ -166,11 +231,18 @@ export class Queue { * @returns {boolean} */ public hasCapacity(): boolean { + // noinspection UnnecessaryLocalVariableJS const hasCapacity = this.slots.length < this.numSlots; - Log.trace("Queue::hasCapacity() - " + this.getName() + "; capacity: " + hasCapacity); return hasCapacity; } + /** + * Move the next job from the waiting queue to the execution queue. + * + * NOTE: this just updates the execution slots, it doesn't actually start the job processing! + * + * @returns {ContainerInput | null} returns the container that should start executing, or null if nothing is available + */ public scheduleNext(): ContainerInput | null { if (this.data.length < 1) { throw new Error("Queue::scheduleNext() - " + this.getName() + " called without anything on the stack."); @@ -183,14 +255,17 @@ export class Queue { return input; } + /** + * @returns {number} the number of jobs currently scheduled to execute + */ public numRunning(): number { return this.slots.length; } public async persist(): Promise { try { - Log.trace("Queue::persist() - saving: " + this.name + " to: " + this.persistDir + - " # slots: " + this.slots.length + "; # data: " + this.data.length); + // Log.trace("Queue::persist() - saving: " + this.name + " to: " + this.persistDir + + // " # slots: " + this.slots.length + "; # data: " + this.data.length); // push current elements back onto the front of the stack const store = {slots: this.slots, data: this.data}; @@ -207,7 +282,7 @@ export class Queue { try { // this happens so infrequently, we will do it synchronously const store = fs.readJSONSync(this.persistDir); - Log.info("Queue::load() - rehydrating: " + this.name + " from: " + this.persistDir); + // Log.info("Queue::load() - rehydrating: " + this.name + " from: " + this.persistDir); Log.info("Queue::load() - rehydrating: " + this.name + "; # slots: " + store.slots.length + "; # data: " + store.data.length); @@ -224,7 +299,7 @@ export class Queue { "; add queued to TAIL: " + data.target.commitURL); this.push(data); // add to the head of the queued list (if we are restarting this will always be true anyways) } - Log.info("Queue::load() - rehydrating: " + this.name + " - done"); + // Log.info("Queue::load() - rehydrating: " + this.name + " - done"); } catch (err) { // if anything happens just don't add to the queue Log.error("Queue::load() - ERROR rehydrating queue: " + err.message); diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index 0baf1ded4..cf2ab94bf 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -45,16 +45,14 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } /** - * Handles push events from Github. + * Handles push events from GitHub. * - * Persists the event so it can be restarted later if needed. + * Persists the event, so it can be restarted later if needed. * - * Schedules the build on the standard queue if there is a default deliverable and it is open. + * Schedules the build on the standard queue if there is a default deliverable, and it is open. * - * - * - * @param info - * @param delivId + * @param {CommitTarget} info + * @param {string} delivId */ public async handlePushEvent(info: CommitTarget, delivId?: string): Promise { try { @@ -99,7 +97,8 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { const shouldPromotePush = await this.classPortal.shouldPromotePush(info); if (shouldPromotePush) { Log.info(`GitHubAutoTest::handlePushEvent(${info.commitSHA}) - Promoting to express queue`); - this.promoteIfNeeded(info); + // this.promoteIfNeeded(info); + this.addToExpressQueue(input); } if (Array.isArray(deliv.regressionDelivIds) && deliv.regressionDelivIds.length > 0) { @@ -116,8 +115,8 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { // regressionInfo.flags.push("#silent"); // avoid posting back regression feedback const regressionInput: ContainerInput = { - delivId: regressionId, - target: regressionInfo, + delivId: regressionId, + target: regressionInfo, containerConfig: regressionDetails }; @@ -212,7 +211,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } else { if (typeof info.flags !== 'undefined') { - // reject #force requests by requetors who are not admins or staff + // reject #force requests by requesters who are not admins or staff if (info.flags.indexOf("#force") >= 0) { Log.warn("GitHubAutoTest::checkCommentPreconditions(..) - ignored, student use of #force"); const msg = "Only admins can use the #force flag."; @@ -264,38 +263,23 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } /** - * Function exists so it can be replaced by tests as needed. + * Function exists as stub for custom course modification of feedback before returning. * + * @param {CommitTarget} target * @param {IGitHubMessage} message * @returns {Promise} */ - protected async postToGitHub(info: CommitTarget, message: IGitHubMessage): Promise { - if (typeof info.flags !== 'undefined' && info.flags.indexOf("#silent") >= 0) { + protected async postToGitHub(target: CommitTarget, message: IGitHubMessage): Promise { + if (typeof target.flags !== 'undefined' && target.flags.indexOf("#silent") >= 0) { Log.info("GitHubAutoTest::postToGitHub(..) - #silent specified; NOT posting message to: " + message.url); } else { Log.info("GitHubAutoTest::postToGitHub(..) - posting message to: " + message.url); - Log.trace("GitHubAutoTest::postToGitHub(..) - info: " + JSON.stringify(info)); + Log.trace("GitHubAutoTest::postToGitHub(..) - target: " + JSON.stringify(target)); Log.trace("GitHubAutoTest::postToGitHub(..) - message: " + JSON.stringify(message)); return await GitHubUtil.postMarkdownToGithub(message); } } - protected async schedule(info: CommitTarget): Promise { - Log.info("GitHubAutoTest::schedule(..) - scheduling for: " + info.personId + - "; delivId: " + info.delivId + "; SHA: " + info.commitURL); - - const containerConfig = await this.classPortal.getContainerDetails(info.delivId); - if (containerConfig !== null) { - const input: ContainerInput = {delivId: info.delivId, target: info, containerConfig}; - this.addToStandardQueue(input); - this.tick(); - Log.info("GitHubAutoTest::schedule(..) - scheduling completed for: " + info.commitURL); - } else { - Log.info("GitHubAutoTest::schedule(..) - scheduling skipped for: " + info.commitURL + - "; no container configuration for: " + info.delivId); - } - } - protected async processComment(info: CommitTarget, res: AutoTestResultTransport): Promise { if (res === null) { return this.processCommentNew(info); @@ -351,142 +335,91 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { Log.info("GitHubAutoTest::processCommentNew(..) - result not yet done; handling for: " + info.personId + "; SHA: " + info.commitSHA); - // not yet processed - const onQueue = this.isOnQueue(info.commitURL, info.delivId); - let msg = ''; - if (onQueue === true) { - msg = "This commit is still queued for processing against " + info.delivId + "."; - msg += " Your results will be posted here as soon as they are ready."; - } else { - const pe = await this.dataStore.getPushRecord(info.commitURL); - if (pe === null) { - Log.warn("GitHubAutoTest::processCommentNew(..) - push event was not present; adding now. URL: " + - info.commitURL + "; for: " + info.personId + "; SHA: " + info.commitSHA); - // store this pushevent for consistency in case we need it for anything else later - await this.dataStore.savePush(info); // NEXT: add cloneURL to commentEvent (should be in github payload) - } - msg = "This commit has been queued for processing against " + info.delivId + "."; - msg += " Your results will be posted here as soon as they are ready."; - // STUDENT: need to guard this - await this.schedule(info); - } - await this.saveCommentInfo(info); - await this.postToGitHub(info, {url: info.postbackURL, message: msg}); - - // jump to head of express queue - this.promoteIfNeeded(info); - - return; - } - protected async processCommentScheduleRequest(info: CommitTarget, res: AutoTestResultTransport): Promise { - Log.info("GitHubAutoTest::processCommentScheduleRequest(..) - handling queue request for user: " + - info.personId + " for commit: " + info.commitURL); + const containerConfig = await this.classPortal.getContainerDetails(info.delivId); + if (containerConfig !== null) { + const input: ContainerInput = {delivId: info.delivId, target: info, containerConfig}; - // Remove any preexisting queued commits - const removedPrevious: ContainerInput | null = this.removeFromScheduleQueue([ - {key: "delivId", value: info.delivId}, - {key: "personId", value: info.personId} - ]); - - let nextTimeslot: number | null = await this.requestNextTimeslot(info.delivId, info.personId); - if (nextTimeslot) { - nextTimeslot += 1; - Log.trace("GitHubAutoTest::processCommentScheduleRequest(..) - Time requested: " + - new Date(info.timestamp).toLocaleTimeString() + "; Time eligible: " + new Date(nextTimeslot).toLocaleTimeString()); - const newTarget: CommitTarget = {...info, timestamp: nextTimeslot}; - const containerConfig = await this.classPortal.getContainerDetails(info.delivId); - let msg: string = ''; - if (containerConfig !== null) { - const input: ContainerInput = {delivId: info.delivId, target: newTarget, containerConfig}; - this.addToScheduleQueue(input); - msg = "Commit scheduled for grading."; - if (removedPrevious) { - msg += `\n\nThis replaces the previously scheduled commit: \`${removedPrevious.target.commitSHA.slice(0, 7)}\`.\n\n`; - } - msg += " Commit will be appended to the grading queue in approximately " + - Util.tookHuman(info.timestamp, nextTimeslot) + ".\n" + - "To replace this commit, call autobot and use `#schedule` again, and to remove it, use `#unschedule`."; + // not yet processed + const onQueue = this.isOnQueue(input); + let msg = ''; + if (onQueue === true) { + msg = "This commit is still queued for processing against " + info.delivId + "."; + msg += " Your results will be posted here as soon as they are ready."; } else { - Log.warn("GitHubAutoTest::processCommentScheduleRequest(..) - commit: " + info.commitSHA + - " - No container info for delivId: " + info.delivId + "; queue ignored."); - msg = "There was an error in queuing this commit. Please contact staff for help."; + const pe = await this.dataStore.getPushRecord(info.commitURL); + if (pe === null) { + Log.warn("GitHubAutoTest::processCommentNew(..) - push event was not present; adding now. URL: " + + info.commitURL + "; for: " + info.personId + "; SHA: " + info.commitSHA); + // store this pushevent for consistency in case we need it for anything else later + await this.dataStore.savePush(info); // NEXT: add cloneURL to commentEvent (should be in github payload) + } + msg = "This commit has been queued for processing against " + info.delivId + "."; + msg += " Your results will be posted here as soon as they are ready."; + + // STUDENT: need to guard this + // await this.schedule(info); } - await this.saveCommentInfo(newTarget); + await this.saveCommentInfo(info); await this.postToGitHub(info, {url: info.postbackURL, message: msg}); - } else { - Log.warn("GitHubActions::processCommentScheduleRequest(..) - nextTimeslot was unexpectedly null." + - " Queuing is now redundant."); - // This is almost certainly unnecessary, but left here to be safe. - await this.processComment(info, res); - } - } - protected async handleCommentUnschedule(info: CommitTarget): Promise { - Log.info("GitHubAutoTest::handleCommentUnschedule(..) - handling student UNschedule request for: " + - info.personId + "; deliv: " + info.delivId + "; for commit: " + info.commitURL); - const res: ContainerInput | null = this.removeFromScheduleQueue([{key: "commitURL", value: info.commitURL}]); - let msg; - if (res) { - Log.info("GitHubAutoTest::handleCommentUnschedule(..) - Unschedule successful for: " + - info.personId + "; deliv: " + info.delivId + "; for commit: " + info.commitURL); - msg = `This commit has successfully been removed from your grading queue.`; + // add to express queue + // this.promoteIfNeeded(info); + this.addToExpressQueue(input); } else { - const onQueue = this.isOnQueue(info.commitURL, info.delivId); - if (onQueue) { - Log.info("GitHubAutoTest::handleCommentUnschedule(..) - Unschedule NOT successful for: " + - info.personId + "; deliv: " + info.delivId + "; for commit: " + info.commitURL + ". Reason: Grading in progess"); - msg = `This commit is already being graded. Your results will be posted here as soon as they are ready.`; - } else { - Log.info("GitHubAutoTest::handleCommentUnschedule(..) - Unschedule NOT successful for: " + - info.personId + "; deliv: " + info.delivId + "; for commit: " + info.commitURL + ". Reason: Not scheduled"); - msg = "This commit is not scheduled to be graded; `#unschedule` is redundant."; - } + Log.warn("GitHubAutoTest::processCommentNew(..) - null container config deliv: " + info.delivId); } - await this.postToGitHub(info, {url: info.postbackURL, message: msg}); + return; } - protected async handleCommentStudent(info: CommitTarget, res: AutoTestResultTransport): Promise { + /** + * Student scheduling requests. + * + * @param {CommitTarget} target + * @param {AutoTestResultTransport} res + * @protected + */ + protected async handleCommentStudent(target: CommitTarget, res: AutoTestResultTransport): Promise { Log.info("GitHubAutoTest::handleCommentStudent(..) - handling student request for: " + - info.personId + "; deliv: " + info.delivId + "; for commit: " + info.commitURL); + target.personId + "; deliv: " + target.delivId + "; for commit: " + target.commitURL); - const shouldCharge = await this.shouldCharge(info, null, res); - const feedbackDelay: string | null = await this.requestFeedbackDelay(info.delivId, info.personId, info.timestamp); - const previousRequest: IFeedbackGiven = await this.dataStore.getFeedbackGivenRecordForCommit(info); + const shouldCharge = await this.shouldCharge(target, null, res); + const feedbackDelay: string | null = await this.requestFeedbackDelay(target.delivId, target.personId, target.timestamp); + const previousRequest: IFeedbackGiven = await this.dataStore.getFeedbackGivenRecordForCommit(target); Log.info("GitHubAutoTest::handleCommentStudent(..) - handling student request for: " + - info.personId + " for commit: " + info.commitURL + "; null previous: " + (previousRequest === null) + + target.personId + " for commit: " + target.commitURL + "; null previous: " + (previousRequest === null) + "; null delay: " + (feedbackDelay === null)); - if (shouldCharge === true && previousRequest === null && feedbackDelay !== null && !info.flags.includes('#schedule')) { - Log.info("GitHubAutoTest::handleCommentStudent(..) - too early for: " + info.personId + "; must wait: " + - feedbackDelay + "; SHA: " + info.commitURL); + if (shouldCharge === true && previousRequest === null && feedbackDelay !== null) { + Log.info("GitHubAutoTest::handleCommentStudent(..) - too early for: " + target.personId + "; must wait: " + + feedbackDelay + "; SHA: " + target.commitURL); // NOPE, not yet (this is the most common case; feedback requested without time constraints) const msg = "You must wait " + feedbackDelay + " before requesting feedback."; - await this.postToGitHub(info, {url: info.postbackURL, message: msg}); - } else if (shouldCharge === true && previousRequest === null && feedbackDelay !== null && info.flags.includes('#schedule')) { - Log.info("GitHubAutoTest::handleCommentStudent(..) - too early for: " + info.personId + "; Scheduling for: " + - feedbackDelay + "; SHA: " + info.commitURL); - // Not yet, but student has requested that autotest be called at the first possible moment. - await this.processCommentScheduleRequest(info, res); + await this.postToGitHub(target, {url: target.postbackURL, message: msg}); } else if (previousRequest !== null) { Log.info("GitHubAutoTest::handleCommentStudent(..) - feedback previously given for: " + - info.personId + "; deliv: " + info.delivId + "; SHA: " + info.commitURL); + target.personId + "; deliv: " + target.delivId + "; SHA: " + target.commitURL); // feedback given before; same as next case but logging is different // processComment will take of whether this is already in progress, etc. - await this.processComment(info, res); + await this.processComment(target, res); + // } else if (target.flags.includes("#check")) { + // NOTE: this was not the real code, I was trying to figure out where this should be + // Log.target("GitHubAutoTest::handleCommentStudent(..) - handling #check"); + // await this.handleCheck(target, res); } else { - Log.info("GitHubAutoTest::handleCommentStudent(..) - not too early; for: " + info.personId + "; SHA: " + info.commitURL); + Log.info("GitHubAutoTest::handleCommentStudent(..) - not too early; for: " + target.personId + "; SHA: " + target.commitURL); // no time limitations. Because of this, queueing is the same as submitting now. // processComment will take of whether this is already in progress, etc. - await this.processComment(info, res); + await this.processComment(target, res); } } private async shouldCharge(info: CommitTarget, isStaff: AutoTestAuthTransport, res: AutoTestResultTransport): Promise { // always false for staff and admins - if (isStaff !== null && (isStaff.isAdmin === true || isStaff.isStaff === true)) { + if (typeof isStaff !== "undefined" && isStaff !== null && + (isStaff.isAdmin === true || isStaff.isStaff === true)) { Log.info("GitHubAutoTest::shouldCharge(..) - false (staff || admin): " + info.personId); return false; } @@ -499,7 +432,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { // false if res exists and has been previously paid for if (res !== null) { - const feedbackRequested: CommitTarget = await this.getRequestor(info.commitURL, info.delivId, 'standard'); + const feedbackRequested: CommitTarget = await this.getRequester(info.commitURL, info.delivId, 'standard'); if (feedbackRequested !== null && feedbackRequested.timestamp < Date.now()) { Log.info("GitHubAutoTest::shouldCharge(..) - false (already paid for)"); return false; @@ -513,7 +446,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { * * NOTE: This description is from an older version of this method. * - * Handles comment events from Github. + * Handles comment events from GitHub. * * Persists the event only if the feedback cannot be given right away and should be given when ready. * @@ -541,6 +474,9 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { Log.info("GitHubAutoTest::handleCommentEvent(..) - start; commenter: " + info.personId + "; SHA: " + info.commitSHA); + Log.trace("GitHubAutoTest::handleCommentEvent(..) - start; comment: " + + JSON.stringify(info)); + // sanity check; this keeps the rest of the code much simpler const preconditionsMet = await this.checkCommentPreconditions(info); if (preconditionsMet === false) { @@ -563,7 +499,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { // in the push. This typically happens on < 5% of student comments. The GitHub API does not provide // a way to recover this information. // - // One hack might be to look at the ref of the preceeding push for this repo, but that is not + // One hack might be to look at the ref of the preceding push for this repo, but that is not // going to be reliable enough to use as a grading criteria for most courses. // // Another approach would be to just comment here and ask for only grading on the pushed commit @@ -576,6 +512,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { if (isStaff !== null && (isStaff.isStaff === true || isStaff.isAdmin === true)) { Log.info("GitHubAutoTest::handleCommentEvent(..) - handleAdmin; for: " + info.personId + "; deliv: " + info.delivId + "; SHA: " + info.commitSHA); + info.adminRequest = true; // set admin request so queues can handle this appropriately if (typeof info.flags !== 'undefined' && info.flags.indexOf("#force") >= 0) { Log.info("GitHubAutoTest::handleCommentEvent(..) - handleAdmin; processing with #force"); await this.processComment(info, null); // do not pass the previous result so a new one will be generated @@ -586,22 +523,19 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } else { Log.info("GitHubAutoTest::handleCommentEvent(..) - handleStudent; for: " + info.personId + "; deliv: " + info.delivId + "; SHA: " + info.commitSHA); - if (typeof info.flags !== 'undefined' && info.flags.indexOf("#unschedule") >= 0) { - await this.handleCommentUnschedule(info); - } else { - await this.handleCommentStudent(info, res); - } + info.adminRequest = false; + await this.handleCommentStudent(info, res); } Log.trace("GitHubAutoTest::handleCommentEvent(..) - done; took: " + Util.took(start)); } - /** - * Returns true if handleCheck runs. False means standard processing should continue. - * - * @param {CommitTarget} info - * @param {AutoTestResultTransport} res - * @returns {Promise} - */ + // /** + // * Returns true if handleCheck runs. False means standard processing should continue. + // * + // * @param {CommitTarget} info + // * @param {AutoTestResultTransport} res + // * @returns {Promise} + // */ // private async handleCheck(info: CommitTarget, res: AutoTestResultTransport): Promise { // if (info !== null && typeof info.flags !== 'undefined' && info.flags.indexOf("#check") >= 0) { // @@ -611,7 +545,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { // return true; // } // - // Log.info("GitHubAutoTest::handleCheck(..) - ignored, #check requested."); + // Log.info("GitHubAutoTest::handleCheck(..) - start; #check requested."); // delete info.flags; // if (res !== null) { // let state = ''; @@ -637,8 +571,8 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { const that = this; const delivId = data.input.delivId; - const standardFeedbackRequested: CommitTarget = await this.getRequestor(data.commitURL, delivId, 'standard'); - const checkFeedbackRequested: CommitTarget = await this.getRequestor(data.commitURL, delivId, 'check'); + const standardFeedbackRequested: CommitTarget = await this.getRequester(data.commitURL, delivId, 'standard'); + const checkFeedbackRequested: CommitTarget = await this.getRequester(data.commitURL, delivId, 'check'); const containerConfig = await this.classPortal.getContainerDetails(delivId); const personId = data.input.target.personId; @@ -653,10 +587,8 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { // intentionally skips calling saveFeedback (because the request should be free) if (futureTarget === true) { - // if #schedule has been requested, remove for this commit because this feedback is being returned for free Log.info(`GitHubAutoTest::processExecution() - postbackOnComplete true;` + `removing ${data.input.target.personId} from scheduleQueue.`); - this.removeFromScheduleQueue([{key: "commitURL", value: data.input.target.commitURL}]); } // do this first, doesn't count against quota Log.info("GitHubAutoTest::processExecution(..) - postback: true; deliv: " + @@ -667,7 +599,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { feedbackDelay === null && futureTarget === false) { // handle user-requested feedback - const giveFeedback = async function(target: CommitTarget, kind: string): Promise { + const giveFeedback = async function (target: CommitTarget, kind: string): Promise { Log.info("GitHubAutoTest::processExecution(..) - " + kind + " feedback requested; deliv: " + delivId + "; repo: " + data.repoId + "; SHA: " + data.commitSHA + '; for: ' + target.personId); const msg = await that.classPortal.formatFeedback(data); @@ -684,7 +616,6 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } } else { // no feedback should be returned - if (feedbackDelay !== null) { Log.info("GitHubAutoTest::processExecution(..) - commit no longer eligible for receiving feedback: " + data.delivId + "; repo: " + data.repoId + "; SHA: " + data.commitSHA + @@ -836,7 +767,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { * @param commitURL * @param delivId */ - private async getRequestor(commitURL: string, delivId: string, kind: string): Promise { + private async getRequester(commitURL: string, delivId: string, kind: string): Promise { try { const record: CommitTarget = await this.dataStore.getCommentRecord(commitURL, delivId, kind); if (record !== null) { @@ -844,7 +775,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } return null; } catch (err) { - Log.error("GitHubAutoTest::getRequestor() - ERROR: " + err); + Log.error("GitHubAutoTest::getRequester() - ERROR: " + err); } } } diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index 1fa2043f0..78d9ddf9b 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -3,8 +3,9 @@ import fetch, {RequestInit} from 'node-fetch'; import Config, {ConfigKey} from "../../../common/Config"; import Log from "../../../common/Log"; -import {CommitTarget} from "../../../common/types/ContainerTypes"; +import {AutoTestAuthTransport} from "../../../common/types/PortalTypes"; import {ClassPortal, IClassPortal} from "../autotest/ClassPortal"; +import {CommitTarget} from "../../../common/types/ContainerTypes"; export interface IGitHubMessage { /** @@ -125,6 +126,13 @@ export class GitHubUtil { // need to get this from portal backend (this is a gitHubId, not a personId) const personResponse = await cp.getPersonId(requestor); // NOTE: this returns Person.id, id, not Person.gitHubId! const personId = personResponse.personId; + + let adminRequest = false; + const authLevel: AutoTestAuthTransport = await cp.isStaff(personId); + if (authLevel.isStaff === true || authLevel.isAdmin === true) { + adminRequest = true; + } + let kind = 'standard'; // if #check, set that here if (flags.indexOf("#check") >= 0) { kind = 'check'; @@ -140,6 +148,7 @@ export class GitHubUtil { commitURL, postbackURL, cloneURL, + adminRequest, personId, kind, timestamp, @@ -224,11 +233,12 @@ export class GitHubUtil { } const pushEvent: CommitTarget = { - delivId: backendConfig.defaultDeliverable, - repoId: repo, + delivId: backendConfig.defaultDeliverable, + repoId: repo, botMentioned: false, // not explicitly invoked - personId: null, // not explicitly requested - kind: 'push', + adminRequest: false, // all pushes are treated equally + personId: null, // not explicitly requested + kind: 'push', cloneURL, commitSHA, commitURL, @@ -277,13 +287,13 @@ export class GitHubUtil { const body: string = JSON.stringify({body: message.message}); const options: RequestInit = { - method: "POST", + method: "POST", headers: { - "Content-Type": "application/json", - "User-Agent": "UBC-AutoTest", + "Content-Type": "application/json", + "User-Agent": "UBC-AutoTest", "Authorization": Config.getInstance().getProp(ConfigKey.githubBotToken) }, - body: body + body: body }; if (Config.getInstance().getProp(ConfigKey.postback) === true) { diff --git a/packages/autotest/test/ClassPortalSpec.ts b/packages/autotest/test/ClassPortalSpec.ts index 06aa61b76..af91f65c3 100644 --- a/packages/autotest/test/ClassPortalSpec.ts +++ b/packages/autotest/test/ClassPortalSpec.ts @@ -215,6 +215,7 @@ describe("ClassPortal Service", () => { commitURL: commitURL, botMentioned: false, + adminRequest: false, personId: null, kind: 'push', diff --git a/packages/autotest/test/GitHubAutoTestSpec.ts b/packages/autotest/test/GitHubAutoTestSpec.ts index 22792c88e..d1d3c11a6 100644 --- a/packages/autotest/test/GitHubAutoTestSpec.ts +++ b/packages/autotest/test/GitHubAutoTestSpec.ts @@ -87,12 +87,12 @@ describe("GitHubAutoTest", () => { }); it("Should be able to receive multiple pushes.", async () => { - expect(at).not.to.equal(null); + expect(at).to.not.be.null; - const pe: CommitTarget = pushes[0]; + // const pe: CommitTarget = pushes[0]; let allData = await data.getAllData(); expect(allData.pushes.length).to.equal(0); - await at.handlePushEvent(pe); + await at.handlePushEvent(pushes[0]); await at.handlePushEvent(pushes[1]); await at.handlePushEvent(pushes[2]); await at.handlePushEvent(pushes[3]); @@ -157,6 +157,7 @@ describe("GitHubAutoTest", () => { Log.test('???'); info = { + adminRequest: false, personId: Config.getInstance().getProp(ConfigKey.botName), botMentioned: true, delivId: 'd1', @@ -235,11 +236,12 @@ describe("GitHubAutoTest", () => { info.personId = student; delete info.flags; - Log.test('schedule and unschedule'); - info.flags = ["#schedule", "#unschedule"]; - meetsPreconditions = await at["checkCommentPreconditions"](info); - expect(meetsPreconditions).to.be.false; - delete info.flags; + // scheduling removed + // Log.test('schedule and unschedule'); + // info.flags = ["#schedule", "#unschedule"]; + // meetsPreconditions = await at["checkCommentPreconditions"](info); + // expect(meetsPreconditions).to.be.false; + // delete info.flags; Log.test('not open yet'); info.timestamp = new Date(2001, 12, 1).getTime(); // not open yet @@ -274,6 +276,7 @@ describe("GitHubAutoTest", () => { botMentioned: true, commitSHA: pe.commitSHA, commitURL: pe.commitURL, + adminRequest: false, personId: "myUser", kind: 'standard', repoId: "d1_project9999", @@ -295,7 +298,7 @@ describe("GitHubAutoTest", () => { // await Util.timeout(1 * 1000); // let test finish so it doesn't ruin subsequent executions }); - it("Should be able to receive a comment event that schedules right away due to no prior request.", async () => { + it("Should be able to receive a comment event that queues right away due to no prior request.", async () => { expect(at).not.to.equal(null); const pe: CommitTarget = pushes[0]; @@ -303,6 +306,7 @@ describe("GitHubAutoTest", () => { botMentioned: true, commitSHA: pe.commitSHA, commitURL: pe.commitURL, + adminRequest: false, personId: "myUser", kind: 'standard', repoId: "d1_project9999", @@ -311,7 +315,7 @@ describe("GitHubAutoTest", () => { timestamp: TS_IN, cloneURL: "https://cloneURL" }; - ce.flags = ["#schedule"]; + ce.flags = []; // ["#schedule"]; Log.test('getting data'); let allData = await data.getAllData(); @@ -372,6 +376,7 @@ describe("GitHubAutoTest", () => { botMentioned: true, commitSHA: pushes[2].commitSHA, commitURL: pushes[2].commitURL, + adminRequest: false, personId: "myUser", kind: 'standard', delivId: "d0", @@ -510,18 +515,20 @@ describe("GitHubAutoTest", () => { // SETUP: add a push with no output records await at.handlePushEvent(TestData.pushEventA); + Log.test("Setup push complete"); let allData = await data.getAllData(); expect(gitHubMessages.length).to.equal(0); // should not be any feedback yet expect(allData.comments.length).to.equal(0); expect(allData.pushes.length).to.equal(1); // don't wait; want to catch this push in flight - Log.test("Setup complete"); + Log.test("Setup validated"); // TEST: send a comment (this is the previous test) const req = Util.clone(TestData.commentRecordUserA) as CommitTarget; req.flags = ["#check"]; req.kind = 'check'; await at.handleCommentEvent(req); + Log.test("Test comment complete"); allData = await data.getAllData(); expect(gitHubMessages.length).to.equal(1); // should generate a warning expect(gitHubMessages[0].message).to.equal("This commit is still queued for processing against d1. Your results will be posted here as soon as they are ready."); @@ -531,6 +538,7 @@ describe("GitHubAutoTest", () => { Log.test("Round 1 complete"); allData = await data.getAllData(); + Log.trace(JSON.stringify(gitHubMessages)); expect(gitHubMessages.length).to.equal(2); // should generate a warning expect(gitHubMessages[1].message).to.equal("Test execution complete."); expect(allData.comments.length).to.equal(1); diff --git a/packages/autotest/test/GitHubEventSpec.ts b/packages/autotest/test/GitHubEventSpec.ts index aeb1adb6a..086f55612 100644 --- a/packages/autotest/test/GitHubEventSpec.ts +++ b/packages/autotest/test/GitHubEventSpec.ts @@ -120,6 +120,7 @@ describe("GitHub Event Parser", () => { postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", timestamp: 1516324553000, botMentioned: false, + adminRequest: false, personId: null, kind: "push", ref: "refs/heads/master" @@ -142,6 +143,7 @@ describe("GitHub Event Parser", () => { repoId: "d1_project9999", timestamp: 1516322017000, botMentioned: false, + adminRequest: false, personId: null, kind: "push", ref: "refs/heads/test2" @@ -172,6 +174,7 @@ describe("GitHub Event Parser", () => { repoId: "d1_project9999", timestamp: 1516324487000, botMentioned: false, + adminRequest: false, personId: null, kind: "push", ref: "refs/heads/test2" @@ -197,6 +200,7 @@ describe("GitHub Event Parser", () => { repoId: "d1_project9999", postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", timestamp: 1516324753000, + adminRequest: false, personId: PERSONID, kind: 'standard', cloneURL: 'https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git', @@ -220,6 +224,7 @@ describe("GitHub Event Parser", () => { commitSHA: "bbe3980fff47b7d6a921e9f89c6727bea639589c", commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/bbe3980fff47b7d6a921e9f89c6727bea639589c", postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", + adminRequest: false, personId: PERSONID, kind: "standard", repoId: "d1_project9999", @@ -244,6 +249,7 @@ describe("GitHub Event Parser", () => { commitSHA: "6da86d2bdfe8fec9120b60e8d7b71c66077489b6", commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/6da86d2bdfe8fec9120b60e8d7b71c66077489b6", postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/6da86d2bdfe8fec9120b60e8d7b71c66077489b6/comments", + adminRequest: false, personId: PERSONID, kind: "standard", repoId: "d1_project9999", @@ -270,6 +276,7 @@ describe("GitHub Event Parser", () => { commitSHA: "d5f2203cfa1ae43a45932511ce39b2368f1c72ed", commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/d5f2203cfa1ae43a45932511ce39b2368f1c72ed", postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", + adminRequest: false, personId: PERSONID, kind: "standard", repoId: "d1_project9999", @@ -285,7 +292,9 @@ describe("GitHub Event Parser", () => { }).timeout(TIMEOUT * 10); function readFile(fName: string): string { - return fs.readFileSync("./test/githubEvents/" + fName, "utf8"); + // NOTE: not sure if this should start with ./packages/... or ./test/... + // ./packages/... works on dev machine + return fs.readFileSync("./packages/autotest/test/githubEvents/" + fName, "utf8"); } }); diff --git a/packages/autotest/test/QueueSpec.ts b/packages/autotest/test/QueueSpec.ts index df8fd5433..7e04aa051 100644 --- a/packages/autotest/test/QueueSpec.ts +++ b/packages/autotest/test/QueueSpec.ts @@ -22,15 +22,15 @@ describe("Queue", () => { }); it("Should work when empty.", () => { - let res: any = q.indexOf('foo'); + let res: any = q.indexOf(TestData.inputRecordA); expect(res).to.equal(-1); res = q.pop(); expect(res).to.be.null; res = q.length(); expect(res).to.equal(0); - res = q.remove('bar'); + res = q.remove(TestData.inputRecordB); expect(res).to.equal(null); - res = q.indexOf('baz'); + res = q.indexOf(TestData.inputRecordB); expect(res).to.equal(-1); }); @@ -46,10 +46,10 @@ describe("Queue", () => { res = q.push(TestData.inputRecordA); expect(res).to.equal(1); - res = q.indexOf(TestData.inputRecordA.target.commitURL); + res = q.indexOf(TestData.inputRecordA); expect(res).to.equal(0); - res = q.remove(TestData.inputRecordA.target.commitURL); + res = q.remove(TestData.inputRecordA); expect(res).to.not.be.null; expect(q.length()).to.equal(0); }); diff --git a/packages/autotest/test/TestData.ts b/packages/autotest/test/TestData.ts index 27e432343..822cfb2f8 100644 --- a/packages/autotest/test/TestData.ts +++ b/packages/autotest/test/TestData.ts @@ -10,6 +10,7 @@ export class TestData { repoId: "d0_team999", timestamp: 1516472872288, botMentioned: false, + adminRequest: false, personId: null, kind: 'push', flags: [], @@ -25,6 +26,7 @@ export class TestData { repoId: "d0_team999", timestamp: 1516992872288, botMentioned: false, + adminRequest: false, personId: null, kind: 'push', flags: [], @@ -41,6 +43,7 @@ export class TestData { repoId: "d0_team999", timestamp: 1516472872288, botMentioned: false, + adminRequest: false, personId: null, kind: 'push', flags: [], @@ -81,6 +84,7 @@ export class TestData { botMentioned: true, commitSHA: "abe1b0918b872997de4c4d2baf4c263f8d4c6dc2", commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2", + adminRequest: false, personId: "cs310test", kind: 'standard', flags: [], @@ -95,6 +99,7 @@ export class TestData { botMentioned: true, commitSHA: "abe1b0918b872997de4c4d2baf4c263f8d4c6dc2", commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2", + adminRequest: false, personId: "cs310test", kind: 'standard', flags: [], @@ -109,6 +114,7 @@ export class TestData { botMentioned: true, commitSHA: "abe1b0918b872997de4c4d2baf4c263f8d4staff", commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4staff", + adminRequest: true, personId: "staff", kind: 'standard', flags: [], diff --git a/packages/common/TestHarness.ts b/packages/common/TestHarness.ts index adac9ac4e..beefa3165 100644 --- a/packages/common/TestHarness.ts +++ b/packages/common/TestHarness.ts @@ -693,6 +693,7 @@ export class Test { commitURL: commitURL, botMentioned: false, + adminRequest: false, personId: null, kind: 'push', diff --git a/packages/common/types/ContainerTypes.ts b/packages/common/types/ContainerTypes.ts index c0a246523..fbd10545a 100644 --- a/packages/common/types/ContainerTypes.ts +++ b/packages/common/types/ContainerTypes.ts @@ -43,8 +43,9 @@ export interface CommitTarget { delivId: string; repoId: string; + adminRequest: boolean; // true if requested by admin or staff botMentioned: boolean; // true if explicitly mentioned - personId: string | null; // string is Person.id if explicily invoked, null otherwise + personId: string | null; // string is Person.id if explicitly invoked, null otherwise kind: string; // kind of request (currently just 'push', 'standard' or 'check') cloneURL: string; diff --git a/tslint.json b/tslint.json index ebe2dd3ee..c8c7e0987 100644 --- a/tslint.json +++ b/tslint.json @@ -23,6 +23,7 @@ "object-literal-sort-keys": false, "no-floating-promises": true, "no-string-literal": false, + "ordered-imports": false, "space-before-function-paren": [ "error", { From 7686756896796747605f7e81d50b3ed263a4d949 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 08:38:07 -0700 Subject: [PATCH 011/104] restore test path change --- packages/autotest/test/GitHubEventSpec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/autotest/test/GitHubEventSpec.ts b/packages/autotest/test/GitHubEventSpec.ts index 086f55612..30ae69097 100644 --- a/packages/autotest/test/GitHubEventSpec.ts +++ b/packages/autotest/test/GitHubEventSpec.ts @@ -294,7 +294,9 @@ describe("GitHub Event Parser", () => { function readFile(fName: string): string { // NOTE: not sure if this should start with ./packages/... or ./test/... // ./packages/... works on dev machine - return fs.readFileSync("./packages/autotest/test/githubEvents/" + fName, "utf8"); + // return fs.readFileSync("./packages/autotest/test/githubEvents/" + fName, "utf8"); + // ./test/... works on CI + return fs.readFileSync("./test/githubEvents/" + fName, "utf8"); } }); From 41499f7fe51798e3c748fdaa133120d76c06906d Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 12:57:20 -0700 Subject: [PATCH 012/104] strengthen autotest test cases --- packages/autotest/src/autotest/AutoTest.ts | 4 +- packages/autotest/src/autotest/Queue.ts | 7 +- .../src/autotest/mocks/MockDataStore.ts | 14 +- packages/autotest/test/GitHubAutoTestSpec.ts | 224 +++++++++--------- packages/autotest/test/GitHubEventSpec.ts | 201 ++++++++-------- packages/autotest/test/GitHubServiceSpec.ts | 13 +- .../test/githubAutoTestData/pushes.json | 27 ++- 7 files changed, 256 insertions(+), 234 deletions(-) diff --git a/packages/autotest/src/autotest/AutoTest.ts b/packages/autotest/src/autotest/AutoTest.ts index e83f87480..2b3be89ba 100644 --- a/packages/autotest/src/autotest/AutoTest.ts +++ b/packages/autotest/src/autotest/AutoTest.ts @@ -184,8 +184,8 @@ export abstract class AutoTest implements IAutoTest { // if job is on any other queue, remove it this.regressionQueue.remove(input); } else { - Log.warn("AutoTest::addToStandardQueue(..) - user: " + - input.target.personId + "; has #" + standardJobCount + + Log.warn("AutoTest::addToStandardQueue(..) - repo: " + + input.target.repoId + "; has #" + standardJobCount + " standard jobs queued and #" + regressionJobCount + " regression jobs queued"); this.addToRegressionQueue(input); diff --git a/packages/autotest/src/autotest/Queue.ts b/packages/autotest/src/autotest/Queue.ts index 7b1df4899..e24519863 100644 --- a/packages/autotest/src/autotest/Queue.ts +++ b/packages/autotest/src/autotest/Queue.ts @@ -170,7 +170,12 @@ export class Queue { for (const job of this.data) { if (input.target.repoId !== null && typeof input.target.repoId !== "undefined" && job.target.repoId === input.target.repoId) { - count++; + if (typeof input.target.adminRequest !== "undefined" && input.target.adminRequest !== null && + input.target.adminRequest === true) { + // admin requests shouldn't count towards repo totals + } else { + count++; + } } } for (const job of this.slots) { diff --git a/packages/autotest/src/autotest/mocks/MockDataStore.ts b/packages/autotest/src/autotest/mocks/MockDataStore.ts index 89e73bf1b..bed0397b4 100644 --- a/packages/autotest/src/autotest/mocks/MockDataStore.ts +++ b/packages/autotest/src/autotest/mocks/MockDataStore.ts @@ -8,6 +8,13 @@ import {CommitTarget} from "../../../../common/types/ContainerTypes"; import Util from "../../../../common/Util"; import {IDataStore} from "../DataStore"; +/** + * NOTE: this can have some unhappy consequences if a job is rapidly reading/writing + * to this datastore w/o waiting appropriately (e.g., if many jobs are processing + * at once in the test suite). + * + * Just know that race conditions are likely, if you are testing that kind of thing. + */ export class MockDataStore implements IDataStore { // NOTE: this creates temp space but does not use the files in autotest/test/githubAutoTestData @@ -73,7 +80,7 @@ export class MockDataStore implements IDataStore { } public async savePush(info: CommitTarget): Promise { - // Log.info("MockDataStore::savePush(..) - start"); + Log.info("MockDataStore::savePush(..) - start"); try { const start = Date.now(); // read @@ -82,8 +89,7 @@ export class MockDataStore implements IDataStore { records.push(info); // write await fs.writeJSON(this.PUSH_PATH, records); - - Log.info("MockDataStore::savePush(..) - done; took: " + Util.took(start)); + Log.info("MockDataStore::savePush(..) - done; #: " + records.length + "; took: " + Util.took(start)); } catch (err) { Log.error("MockDataStore::savePush(..) - ERROR: " + err); } @@ -205,7 +211,7 @@ export class MockDataStore implements IDataStore { Log.info("MockDataStore::getLatestFeedbackGivenRecord(..) - not found; took: " + Util.took(start)); ret = null; } else { - Math.max.apply(Math, shortList.map(function(o: IFeedbackGiven) { + Math.max.apply(Math, shortList.map(function (o: IFeedbackGiven) { Log.info("MockDataStore::getLatestFeedbackGivenRecord(..) - found; took: " + Util.took(start)); ret = o; })); diff --git a/packages/autotest/test/GitHubAutoTestSpec.ts b/packages/autotest/test/GitHubAutoTestSpec.ts index d1d3c11a6..3c9924755 100644 --- a/packages/autotest/test/GitHubAutoTestSpec.ts +++ b/packages/autotest/test/GitHubAutoTestSpec.ts @@ -5,7 +5,6 @@ import "mocha"; import Config, {ConfigKey} from "../../common/Config"; import Log from "../../common/Log"; -import {Test} from "../../common/TestHarness"; import {IFeedbackGiven} from "../../common/types/AutoTestTypes"; import {CommitTarget} from "../../common/types/ContainerTypes"; import Util from "../../common/Util"; @@ -30,8 +29,8 @@ describe("GitHubAutoTest", () => { let at: GitHubAutoTest; const TS_IN = new Date(2018, 3, 3).getTime(); - const TS_BEFORE = new Date(2017, 12, 12).getTime(); - const TS_AFTER = new Date(2018, 12, 12).getTime(); + // const TS_BEFORE = new Date(2017, 12, 12).getTime(); + // const TS_AFTER = new Date(2018, 12, 12).getTime(); const WAIT = 1000; @@ -39,7 +38,7 @@ describe("GitHubAutoTest", () => { // now -10h: 1516523258762 // now - 24h: 1516472872288 - before(async function() { + before(async function () { Log.test("AutoTest::before() - start"); pushes = fs.readJSONSync(__dirname + "/githubAutoTestData/pushes.json"); @@ -58,14 +57,14 @@ describe("GitHubAutoTest", () => { Log.test("AutoTest::before() - done"); }); - beforeEach(async function() { + beforeEach(async function () { await data.clearData(); // create a new AutoTest every test (allows us to mess with methods and make sure they are called) at = new GitHubAutoTest(data, portal, null); // , gh); }); - afterEach(async function() { + afterEach(async function () { // pause after each test so async issues don't persist // this is a hack, but makes the tests more deterministic // Log.test("AutoTest::afterEach() - start"); @@ -89,7 +88,6 @@ describe("GitHubAutoTest", () => { it("Should be able to receive multiple pushes.", async () => { expect(at).to.not.be.null; - // const pe: CommitTarget = pushes[0]; let allData = await data.getAllData(); expect(allData.pushes.length).to.equal(0); await at.handlePushEvent(pushes[0]); @@ -102,30 +100,72 @@ describe("GitHubAutoTest", () => { expect(allData.pushes.length).to.equal(6); }); - it("Should be able to receive multiple concurrent pushes.", async () => { - try { - expect(at).not.to.equal(null); - - // const pe: IPushEvent = pushes[0]; - const arr = []; - arr.push(at.handlePushEvent(pushes[0])); - arr.push(at.handlePushEvent(pushes[1])); - arr.push(at.handlePushEvent(pushes[2])); - arr.push(at.handlePushEvent(pushes[3])); - arr.push(at.handlePushEvent(pushes[4])); - arr.push(at.handlePushEvent(pushes[5])); - - Log.test('multiple concurrent pushes - before promise.all'); - await Promise.all(arr); - Log.test('multiple concurrent pushes - after promise.all'); - // const allData = await data.getAllData(); - // Log.test('after getAllData'); - // expect(allData.pushes.length).to.equal(6); - } catch (err) { - Log.error('multiple concurrent pushes - should never happen: ' + err); - expect.fail('multiple concurrent pushes - should never happen', err); - } - }); + // this works, but with the filesystem-backed mock data store, race conditions + // keep happening + // it("Admin requests should go to the front of the queue.", async () => { + // expect(at).to.not.be.null; + // + // let allData = await data.getAllData(); + // expect(allData.pushes.length).to.equal(0); + // await at.handlePushEvent(pushes[0]); + // await at.handlePushEvent(pushes[1]); + // await at.handlePushEvent(pushes[2]); + // await at.handlePushEvent(pushes[3]); + // await at.handlePushEvent(pushes[4]); + // const push = Object.assign({}, pushes[5]); + // push.commitSHA = "admin_" + push.commitSHA; + // push.adminRequest = true; + // push.personId = "admin1"; + // await at.handlePushEvent(push); + // allData = await data.getAllData(); + // expect(allData.pushes.length).to.equal(6); + // + // await Util.delay(WAIT * 5); + // allData = await data.getAllData(); + // expect(allData.pushes.length).to.equal(6); // unchanged + // expect(allData.records.length).to.equal(6); + // // as long as the last record isn't the admin record it must have been bumped up + // expect(allData.records[5].input.target.adminRequest).to.be.undefined; + // // need to wait longer really, but + // + // // TODO, check all data to make sure the admin push processed first + // }).timeout(WAIT * 6); + + it("Rapid requests should go to the regression queue.", async () => { + expect(at).to.not.be.null; + + let allData = await data.getAllData(); + expect(allData.pushes.length).to.equal(0); + await at.handlePushEvent(pushes[0]); + await at.handlePushEvent(pushes[1]); + await at.handlePushEvent(pushes[2]); + await at.handlePushEvent(pushes[3]); + await at.handlePushEvent(pushes[4]); + await at.handlePushEvent(pushes[5]); + + // to see at what admin pushes look like + // const push = Object.assign({}, pushes[5]); + // push.commitSHA = "admin_" + push.commitSHA; + // push.adminRequest = true; + // push.personId = "admin1"; + // await at.handlePushEvent(push); + + allData = await data.getAllData(); + + await Util.delay(10); + + // all pushes should be here + expect(allData.pushes.length).to.equal(6); + const eq = (at["expressQueue"] as any); + const sq = (at["standardQueue"] as any); + const rq = (at["regressionQueue"] as any); + expect(eq.slots).to.have.length(0); // nothing should be running on express + expect(eq.data).to.have.length(0); // nothing should be queued on express + expect(sq.slots).to.have.length(2); // two should be running on standard + expect(sq.data).to.have.length(2); // two should be waiting on standard + expect(rq.slots).to.have.length(1); // one should be running on regression + expect(rq.data).to.have.length(1); // one should be queued on regression + }).timeout(WAIT * 2); it("Should gracefully fail with bad comments.", async () => { expect(at).not.to.equal(null); @@ -158,16 +198,16 @@ describe("GitHubAutoTest", () => { Log.test('???'); info = { adminRequest: false, - personId: Config.getInstance().getProp(ConfigKey.botName), + personId: Config.getInstance().getProp(ConfigKey.botName), botMentioned: true, - delivId: 'd1', - kind: 'standard', - repoId: 'repoId', - commitSHA: 'SHA', - commitURL: 'https://URL', - postbackURL: 'https://postback', - timestamp: new Date(2018, 2, 1).getTime(), - cloneURL: "https://cloneURL" + delivId: 'd1', + kind: 'standard', + repoId: 'repoId', + commitSHA: 'SHA', + commitURL: 'https://URL', + postbackURL: 'https://postback', + timestamp: new Date(2018, 2, 1).getTime(), + cloneURL: "https://cloneURL" }; meetsPreconditions = await at["checkCommentPreconditions"](info); expect(meetsPreconditions).to.be.false; @@ -236,13 +276,6 @@ describe("GitHubAutoTest", () => { info.personId = student; delete info.flags; - // scheduling removed - // Log.test('schedule and unschedule'); - // info.flags = ["#schedule", "#unschedule"]; - // meetsPreconditions = await at["checkCommentPreconditions"](info); - // expect(meetsPreconditions).to.be.false; - // delete info.flags; - Log.test('not open yet'); info.timestamp = new Date(2001, 12, 1).getTime(); // not open yet meetsPreconditions = await at["checkCommentPreconditions"](info); @@ -274,16 +307,16 @@ describe("GitHubAutoTest", () => { const pe: CommitTarget = pushes[0]; const ce: CommitTarget = { botMentioned: true, - commitSHA: pe.commitSHA, - commitURL: pe.commitURL, + commitSHA: pe.commitSHA, + commitURL: pe.commitURL, adminRequest: false, - personId: "myUser", - kind: 'standard', - repoId: "d1_project9999", - delivId: "d0", - postbackURL: "https://github.students.cs.ubc.ca/api/v3/repos/classytest/PostTestDoNotDelete/commit/c35a0e5968338a9757813b58368f36ddd64b063e/comments", - timestamp: TS_IN, - cloneURL: "https://cloneURL" + personId: "myUser", + kind: 'standard', + repoId: "d1_project9999", + delivId: "d0", + postbackURL: "https://github.students.cs.ubc.ca/api/v3/repos/classytest/PostTestDoNotDelete/commit/c35a0e5968338a9757813b58368f36ddd64b063e/comments", + timestamp: TS_IN, + cloneURL: "https://cloneURL" }; Log.test('getting data'); @@ -304,18 +337,18 @@ describe("GitHubAutoTest", () => { const pe: CommitTarget = pushes[0]; const ce: CommitTarget = { botMentioned: true, - commitSHA: pe.commitSHA, - commitURL: pe.commitURL, + commitSHA: pe.commitSHA, + commitURL: pe.commitURL, adminRequest: false, - personId: "myUser", - kind: 'standard', - repoId: "d1_project9999", - delivId: "d0", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", - timestamp: TS_IN, - cloneURL: "https://cloneURL" + personId: "myUser", + kind: 'standard', + repoId: "d1_project9999", + delivId: "d0", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", + timestamp: TS_IN, + cloneURL: "https://cloneURL" }; - ce.flags = []; // ["#schedule"]; + ce.flags = []; Log.test('getting data'); let allData = await data.getAllData(); @@ -329,39 +362,6 @@ describe("GitHubAutoTest", () => { expect(allData.comments.length).to.equal(1); }); - // TODO: need to strengthen this with a new feedback record in - // it("Should be able to receive a comment event that schedules for the future due to prior requests.", async () => { - // expect(at).not.to.equal(null); - // - // const pe: CommitTarget = pushes[0]; - // const ce: CommitTarget = { - // botMentioned: true, - // commitSHA: pe.commitSHA, - // commitURL: pe.commitURL, - // personId: "cs310test", - // kind: 'standard', - // repoId: "d1_project9999", - // delivId: "d1", - // postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", - // timestamp: TS_IN, - // cloneURL: "https://cloneURL" - // }; - // ce.flags = ["#schedule"]; - // - // Log.test('getting data'); - // let allData = await data.getAllData(); - // expect(allData.comments.length).to.equal(0); - // expect(allData.feedback.length).to.equal(1); // should be one record in there for this user - // - // Log.test('handling schedule'); - // ce.timestamp = TS_IN + 1000; - // await at.handleCommentEvent(ce); - // - // Log.test('re-getting data'); - // allData = await data.getAllData(); - // expect(allData.comments.length).to.equal(1); - // }); - it("Should be able to use a comment event to start the express queue.", async () => { expect(at).not.to.equal(null); @@ -374,16 +374,16 @@ describe("GitHubAutoTest", () => { const ce: CommitTarget = { botMentioned: true, - commitSHA: pushes[2].commitSHA, - commitURL: pushes[2].commitURL, + commitSHA: pushes[2].commitSHA, + commitURL: pushes[2].commitURL, adminRequest: false, - personId: "myUser", - kind: 'standard', - delivId: "d0", - repoId: "d1_project9999", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/cbe1b0918b872997de4c4d2baf4c263f8d4c6dc2/comments", - timestamp: TS_IN, - cloneURL: "https://cloneURL" + personId: "myUser", + kind: 'standard', + delivId: "d0", + repoId: "d1_project9999", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/cbe1b0918b872997de4c4d2baf4c263f8d4c6dc2/comments", + timestamp: TS_IN, + cloneURL: "https://cloneURL" }; await Util.delay(WAIT); @@ -400,7 +400,7 @@ describe("GitHubAutoTest", () => { function stubDependencies() { gitHubMessages = []; - at["postToGitHub"] = function(info: CommitTarget, message: IGitHubMessage): Promise { + at["postToGitHub"] = function (info: CommitTarget, message: IGitHubMessage): Promise { Log.test("stubbed postToGitHub(..) - message: " + JSON.stringify(message)); Log.test(gitHubMessages.length + ''); if (typeof info.flags === 'undefined' || info.flags.indexOf("#silent") < 0) { @@ -662,10 +662,10 @@ describe("GitHubAutoTest", () => { // SETUP: add a push with no output records const fg: IFeedbackGiven = { commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263fSOMEOTHER", // different commit - delivId: "d1", // same deliverable + delivId: "d1", // same deliverable timestamp: TestData.commentRecordUserA.timestamp, // 1516451273288, - personId: "cs310test", - kind: 'standard' + personId: "cs310test", + kind: 'standard' }; await data.savePush(TestData.inputRecordA.target); diff --git a/packages/autotest/test/GitHubEventSpec.ts b/packages/autotest/test/GitHubEventSpec.ts index 30ae69097..0611fd1cb 100644 --- a/packages/autotest/test/GitHubEventSpec.ts +++ b/packages/autotest/test/GitHubEventSpec.ts @@ -24,7 +24,7 @@ describe("GitHub Event Parser", () => { const TIMEOUT = 1000; let backend: BackendServer = null; - before(async function() { + before(async function () { Log.test("GitHubEventParserSpec::before() - start"); backend = new BackendServer(); await backend.start(); @@ -33,15 +33,15 @@ describe("GitHub Event Parser", () => { const id = PERSONID; const githubId = GITHUBID; const p: Person = { - id: id, - csId: id, - githubId: githubId, + id: id, + csId: id, + githubId: githubId, studentNumber: null, fName: 'f' + id, lName: 'l' + id, - kind: null, - URL: null, + kind: null, + URL: null, labId: null, @@ -56,30 +56,30 @@ describe("GitHub Event Parser", () => { const deliv: Deliverable = { id: 'd4', - URL: 'http://NOTSET', - openTimestamp: new Date(1400000000000).getTime(), + URL: 'http://NOTSET', + openTimestamp: new Date(1400000000000).getTime(), closeTimestamp: new Date(1500000000000).getTime(), gradesReleased: false, - shouldProvision: true, - importURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/classytest/' + Test.REPONAMEREAL_POSTTEST + '.git', - teamMinSize: 2, - teamMaxSize: 2, - teamSameLab: true, + shouldProvision: true, + importURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/classytest/' + Test.REPONAMEREAL_POSTTEST + '.git', + teamMinSize: 2, + teamMaxSize: 2, + teamSameLab: true, teamStudentsForm: true, - teamPrefix: 't', - repoPrefix: '', + teamPrefix: 't', + repoPrefix: '', visibleToStudents: true, - lateAutoTest: false, + lateAutoTest: false, shouldAutoTest: true, - autotest: { - dockerImage: 'testImage', - studentDelay: 60 * 60 * 12, // 12h - maxExecTime: 300, + autotest: { + dockerImage: 'testImage', + studentDelay: 60 * 60 * 12, // 12h + maxExecTime: 300, regressionDelivIds: [], - custom: {} + custom: {} }, rubric: {}, @@ -91,7 +91,7 @@ describe("GitHub Event Parser", () => { Log.test("GitHubEventParserSpec::before() - done"); }); - after(async function() { + after(async function () { Log.test("GitHubEventParserSpec::after() - start"); await backend.stop(); Log.test("GitHubEventParserSpec::after() - done"); @@ -112,18 +112,18 @@ describe("GitHub Event Parser", () => { // Log.test(JSON.stringify(actual)); const expected: CommitTarget = { - delivId: "d1", - repoId: "d1_project9999", - cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", - commitSHA: "bbe3980fff47b7d6a921e9f89c6727bea639589c", - commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/bbe3980fff47b7d6a921e9f89c6727bea639589c", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", - timestamp: 1516324553000, + delivId: "d1", + repoId: "d1_project9999", + cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", + commitSHA: "bbe3980fff47b7d6a921e9f89c6727bea639589c", + commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/bbe3980fff47b7d6a921e9f89c6727bea639589c", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", + timestamp: 1516324553000, botMentioned: false, adminRequest: false, - personId: null, - kind: "push", - ref: "refs/heads/master" + personId: null, + kind: "push", + ref: "refs/heads/master" }; delete expected.timestamp; delete actual.timestamp; @@ -135,18 +135,18 @@ describe("GitHub Event Parser", () => { const actual = await GitHubUtil.processPush(JSON.parse(content), new MockClassPortal()); const expected: CommitTarget = { - delivId: "d1", - cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", - commitSHA: "6da86d2bdfe8fec9120b60e8d7b71c66077489b6", - commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/6da86d2bdfe8fec9120b60e8d7b71c66077489b6", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/6da86d2bdfe8fec9120b60e8d7b71c66077489b6/comments", - repoId: "d1_project9999", - timestamp: 1516322017000, + delivId: "d1", + cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", + commitSHA: "6da86d2bdfe8fec9120b60e8d7b71c66077489b6", + commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/6da86d2bdfe8fec9120b60e8d7b71c66077489b6", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/6da86d2bdfe8fec9120b60e8d7b71c66077489b6/comments", + repoId: "d1_project9999", + timestamp: 1516322017000, botMentioned: false, adminRequest: false, - personId: null, - kind: "push", - ref: "refs/heads/test2" + personId: null, + kind: "push", + ref: "refs/heads/test2" }; delete expected.timestamp; delete actual.timestamp; @@ -161,23 +161,23 @@ describe("GitHub Event Parser", () => { expect(actual).to.equal(expected); // nothing to do when a branch is deleted }); - it("Should be able to parse a push to a branch.", async function() { + it("Should be able to parse a push to a branch.", async function () { const content = readFile("push_other-branch.json"); const actual = await GitHubUtil.processPush(JSON.parse(content), new MockClassPortal()); const expected: CommitTarget = { - delivId: "d1", - commitSHA: "d5f2203cfa1ae43a45932511ce39b2368f1c72ed", - cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", - commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/d5f2203cfa1ae43a45932511ce39b2368f1c72ed", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", - repoId: "d1_project9999", - timestamp: 1516324487000, + delivId: "d1", + commitSHA: "d5f2203cfa1ae43a45932511ce39b2368f1c72ed", + cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", + commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/d5f2203cfa1ae43a45932511ce39b2368f1c72ed", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", + repoId: "d1_project9999", + timestamp: 1516324487000, botMentioned: false, adminRequest: false, - personId: null, - kind: "push", - ref: "refs/heads/test2" + personId: null, + kind: "push", + ref: "refs/heads/test2" }; delete expected.timestamp; @@ -185,7 +185,7 @@ describe("GitHub Event Parser", () => { expect(actual).to.deep.equal(expected); }); - it("Should be able to parse a comment on a master commit with one deliverable and a mention.", async function() { + it("Should be able to parse a comment on a master commit with one deliverable and a mention.", async function () { const content = JSON.parse(readFile("comment_master_bot_one-deliv.json")); const botname = Config.getInstance().getProp(ConfigKey.botName); content.comment.body = content.comment.body.replace('ubcbot', botname); @@ -194,17 +194,17 @@ describe("GitHub Event Parser", () => { const expected: CommitTarget = { botMentioned: true, - commitSHA: "bbe3980fff47b7d6a921e9f89c6727bea639589c", - commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/bbe3980fff47b7d6a921e9f89c6727bea639589c", - delivId: "d4", - repoId: "d1_project9999", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", - timestamp: 1516324753000, + commitSHA: "bbe3980fff47b7d6a921e9f89c6727bea639589c", + commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/bbe3980fff47b7d6a921e9f89c6727bea639589c", + delivId: "d4", + repoId: "d1_project9999", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", + timestamp: 1516324753000, adminRequest: false, - personId: PERSONID, - kind: 'standard', - cloneURL: 'https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git', - flags: ["#d4"] + personId: PERSONID, + kind: 'standard', + cloneURL: 'https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git', + flags: ["#d4"] }; delete expected.timestamp; @@ -221,17 +221,17 @@ describe("GitHub Event Parser", () => { const expected: CommitTarget = { botMentioned: true, - commitSHA: "bbe3980fff47b7d6a921e9f89c6727bea639589c", - commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/bbe3980fff47b7d6a921e9f89c6727bea639589c", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", + commitSHA: "bbe3980fff47b7d6a921e9f89c6727bea639589c", + commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/bbe3980fff47b7d6a921e9f89c6727bea639589c", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/bbe3980fff47b7d6a921e9f89c6727bea639589c/comments", adminRequest: false, - personId: PERSONID, - kind: "standard", - repoId: "d1_project9999", - delivId: "d4", - timestamp: 1516324833000, - cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", - flags: ["#d7", "#d4"] + personId: PERSONID, + kind: "standard", + repoId: "d1_project9999", + delivId: "d4", + timestamp: 1516324833000, + cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", + flags: ["#d7", "#d4"] }; delete expected.timestamp; @@ -246,17 +246,17 @@ describe("GitHub Event Parser", () => { const expected: CommitTarget = { botMentioned: false, - commitSHA: "6da86d2bdfe8fec9120b60e8d7b71c66077489b6", - commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/6da86d2bdfe8fec9120b60e8d7b71c66077489b6", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/6da86d2bdfe8fec9120b60e8d7b71c66077489b6/comments", + commitSHA: "6da86d2bdfe8fec9120b60e8d7b71c66077489b6", + commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/6da86d2bdfe8fec9120b60e8d7b71c66077489b6", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/6da86d2bdfe8fec9120b60e8d7b71c66077489b6/comments", adminRequest: false, - personId: PERSONID, - kind: "standard", - repoId: "d1_project9999", - delivId: null, - timestamp: 1516320674000, - cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", - flags: [] + personId: PERSONID, + kind: "standard", + repoId: "d1_project9999", + delivId: null, + timestamp: 1516320674000, + cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", + flags: [] }; delete expected.timestamp; @@ -273,17 +273,17 @@ describe("GitHub Event Parser", () => { const expected: CommitTarget = { botMentioned: true, - commitSHA: "d5f2203cfa1ae43a45932511ce39b2368f1c72ed", - commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/d5f2203cfa1ae43a45932511ce39b2368f1c72ed", - postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", + commitSHA: "d5f2203cfa1ae43a45932511ce39b2368f1c72ed", + commitURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999/commit/d5f2203cfa1ae43a45932511ce39b2368f1c72ed", + postbackURL: "https://github.ugrad.cs.ubc.ca/api/v3/repos/CPSC310-2017W-T2/d1_project9999/commits/d5f2203cfa1ae43a45932511ce39b2368f1c72ed/comments", adminRequest: false, - personId: PERSONID, - kind: "standard", - repoId: "d1_project9999", - delivId: "d4", - timestamp: 1516324931000, - cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", - flags: ["#d4"] + personId: PERSONID, + kind: "standard", + repoId: "d1_project9999", + delivId: "d4", + timestamp: 1516324931000, + cloneURL: "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d1_project9999.git", + flags: ["#d4"] }; delete expected.timestamp; @@ -292,11 +292,14 @@ describe("GitHub Event Parser", () => { }).timeout(TIMEOUT * 10); function readFile(fName: string): string { - // NOTE: not sure if this should start with ./packages/... or ./test/... - // ./packages/... works on dev machine - // return fs.readFileSync("./packages/autotest/test/githubEvents/" + fName, "utf8"); - // ./test/... works on CI - return fs.readFileSync("./test/githubEvents/" + fName, "utf8"); + // test root folders depend on the environment executing the test case + if (Test.isCI() === true) { + // ./test/... works on CI + return fs.readFileSync("./test/githubEvents/" + fName, "utf8"); + } else { + // ./packages/... works on dev machine + return fs.readFileSync("./packages/autotest/test/githubEvents/" + fName, "utf8"); + } } }); diff --git a/packages/autotest/test/GitHubServiceSpec.ts b/packages/autotest/test/GitHubServiceSpec.ts index b3682f089..ce8de20d3 100644 --- a/packages/autotest/test/GitHubServiceSpec.ts +++ b/packages/autotest/test/GitHubServiceSpec.ts @@ -3,7 +3,6 @@ import "mocha"; import Config, {ConfigKey} from "../../common/Config"; import Log from "../../common/Log"; -import {Test} from "../../common/TestHarness"; import {GitHubUtil, IGitHubMessage} from "../src/github/GitHubUtil"; import "./GlobalSpec"; @@ -21,7 +20,7 @@ describe("GitHub Markdown Service", () => { const postbackVal = Config.getInstance().getProp(ConfigKey.postback); - before(function() { + before(function () { // gh = new GitHubService(); // set postback to be true so we an actually validate this @@ -29,15 +28,15 @@ describe("GitHub Markdown Service", () => { config.setProp(ConfigKey.postback, true); }); - after(function() { + after(function () { // return postback val const config = Config.getInstance(); config.setProp(ConfigKey.postback, postbackVal); }); - it("Should be able to post a valid message.", async function() { + it("Should be able to post a valid message.", async function () { const post: IGitHubMessage = { - url: VALID_URL, + url: VALID_URL, message: "Automated Test Suite Message" }; @@ -50,9 +49,9 @@ describe("GitHub Markdown Service", () => { Log.test("Failure (unexpected)"); expect.fail(); } - }).timeout(TIMEOUT); + }).timeout(TIMEOUT * 2); - it("Should fail when trying to post an invalid message.", async function() { + it("Should fail when trying to post an invalid message.", async function () { const post: any = { url: VALID_URL }; diff --git a/packages/autotest/test/githubAutoTestData/pushes.json b/packages/autotest/test/githubAutoTestData/pushes.json index 17453ed4e..28e800b3a 100644 --- a/packages/autotest/test/githubAutoTestData/pushes.json +++ b/packages/autotest/test/githubAutoTestData/pushes.json @@ -5,7 +5,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/abe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 1234567890 }, { @@ -14,7 +15,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/bbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 2234567890 }, { @@ -23,7 +25,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/cbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 3234567890 }, { @@ -32,7 +35,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/dbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 4234567890 }, { @@ -41,7 +45,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/ebe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 5234567890 }, { @@ -50,7 +55,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/fbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 6234567890 }, { @@ -59,7 +65,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/gbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 7234567890 }, { @@ -68,7 +75,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/hbe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 8234567890 }, { @@ -77,7 +85,8 @@ "commitURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/commit/ibe1b0918b872997de4c4d2baf4c263f8d4c6dc2", "projectURL": "https://github.ugrad.cs.ubc.ca/CPSC310-2017W-T2/d0_team999/", "postbackURL": "EMPTY", - "repo": "d0_team999", + "repoId": "d0_team999", + "personId": "user1", "timestamp": 9234567890 } ] From fb598c2e09af7f3c31d62daf0417a60329c66a45 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 14:03:59 -0700 Subject: [PATCH 013/104] migrate to modern onsen; fix broken icons on the admin views --- packages/portal/frontend/html/index.html | 6 +++--- packages/portal/frontend/package.json | 2 +- .../default/portal/frontend/html/admin.html | 16 +++++++------- .../example/portal/frontend/html/admin.html | 16 +++++++------- yarn.lock | 21 ++++--------------- 5 files changed, 24 insertions(+), 37 deletions(-) diff --git a/packages/portal/frontend/html/index.html b/packages/portal/frontend/html/index.html index 965105576..2e8348da6 100644 --- a/packages/portal/frontend/html/index.html +++ b/packages/portal/frontend/html/index.html @@ -5,9 +5,9 @@ Classy - - - + + + diff --git a/packages/portal/frontend/package.json b/packages/portal/frontend/package.json index 50ada8588..dcfd0f12d 100644 --- a/packages/portal/frontend/package.json +++ b/packages/portal/frontend/package.json @@ -30,7 +30,7 @@ "fs-extra": "^5.0.0", "moment": "^2.29.4", "mongodb": "^3.0.2", - "onsenui": "^2.9.2", + "onsenui": "^2.12.2", "restify": "^8.4.0", "source-map-loader": "^0.2.4", "ts-loader": "^9.2.3", diff --git a/plugins/default/portal/frontend/html/admin.html b/plugins/default/portal/frontend/html/admin.html index 2f4e31b7e..027fe1d91 100644 --- a/plugins/default/portal/frontend/html/admin.html +++ b/plugins/default/portal/frontend/html/admin.html @@ -17,19 +17,19 @@ - + - + - + - + - + @@ -538,7 +538,7 @@
- +
URL @@ -661,7 +661,7 @@ Deliverable Dates
- +
Open @@ -680,7 +680,7 @@
- +
Close diff --git a/plugins/example/portal/frontend/html/admin.html b/plugins/example/portal/frontend/html/admin.html index 2f4e31b7e..027fe1d91 100644 --- a/plugins/example/portal/frontend/html/admin.html +++ b/plugins/example/portal/frontend/html/admin.html @@ -17,19 +17,19 @@ - + - + - + - + - + @@ -538,7 +538,7 @@
- +
URL @@ -661,7 +661,7 @@ Deliverable Dates
- +
Open @@ -680,7 +680,7 @@
- +
Close diff --git a/yarn.lock b/yarn.lock index 52a072eac..b46069332 100644 --- a/yarn.lock +++ b/yarn.lock @@ -300,11 +300,6 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@onsenui/custom-elements@1.0.0": - version "1.0.0" - resolved "https://registry.nlark.com/@onsenui/custom-elements/download/@onsenui/custom-elements-1.0.0.tgz#8e2b5c8968b8d94da70c6e92ecc56b47f79a0ff3" - integrity sha1-jitciWi42U2nDG6S7MVrR/eaD/M= - "@servie/events@^1.0.0": version "1.0.0" resolved "https://registry.nlark.com/@servie/events/download/@servie/events-1.0.0.tgz#8258684b52d418ab7b86533e861186638ecc5dc1" @@ -1358,11 +1353,6 @@ copy-webpack-plugin@^6.2.1: serialize-javascript "^5.0.1" webpack-sources "^1.4.3" -core-js@^2.6.12: - version "2.6.12" - resolved "https://registry.nlark.com/core-js/download/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha1-2TM9+nsGXjR8xWgiGdb2kIWcwuw= - core-js@^3.1.3: version "3.16.1" resolved "https://registry.nlark.com/core-js/download/core-js-3.16.1.tgz#f4485ce5c9f3c6a7cb18fa80488e08d362097249" @@ -3241,13 +3231,10 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onsenui@^2.9.2: - version "2.11.2" - resolved "https://registry.nlark.com/onsenui/download/onsenui-2.11.2.tgz#acec927bccadc83eb882d3eb9cff965bfe8aae63" - integrity sha1-rOySe8ytyD64gtPrnP+WW/6KrmM= - dependencies: - "@onsenui/custom-elements" "1.0.0" - core-js "^2.6.12" +onsenui@^2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/onsenui/-/onsenui-2.12.2.tgz#8ab9cb1912280fc5da6a72555bde34444f027198" + integrity sha512-+PIEhXlBkESf8ZcsvFd5+bjNrVaZGLA7/3H7sL6GPB+f668sFR2D7bIgaT5DNGPaLq97Zq5VbmZnaPP6sfGWBg== optional-require@^1.0.3: version "1.1.7" From a50e200fe6ef5d359ce6db754d3abde35ad095d5 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 15:10:27 -0700 Subject: [PATCH 014/104] grade and result number alignment --- .../frontend/src/app/views/AdminGradesTab.ts | 48 ++++++---- .../frontend/src/app/views/AdminResultsTab.ts | 92 +++++++++++-------- 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/packages/portal/frontend/src/app/views/AdminGradesTab.ts b/packages/portal/frontend/src/app/views/AdminGradesTab.ts index e2ab8873f..2c6d2aa08 100644 --- a/packages/portal/frontend/src/app/views/AdminGradesTab.ts +++ b/packages/portal/frontend/src/app/views/AdminGradesTab.ts @@ -102,7 +102,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + style: 'padding-left: 1em; padding-right: 1em; text-align: right;' }; headers.push(col); } @@ -128,9 +128,23 @@ export class AdminGradesTab extends AdminPage { if (grade.personId === student.id) { if (grade.delivId === deliv.id) { const hoverComment = AdminGradesTab.makeHTMLSafe(grade.comment); - let score = ''; + // let score = ''; + let score: number | string = ''; if (grade.score !== null && grade.score >= 0) { - score = grade.score + ''; + score = grade.score; + if (score === 100) { + score = "100.00"; + } else { + // two decimal places + score = score.toFixed(2); + // prepend space (not 100) + score = " " + score; + if (grade.score < 10) { + // prepend with extra space if < 10 + score = " " + score; + } + } + // score = grade.score + ''; } let html; if (score !== '' && grade.URL !== null) { @@ -178,7 +192,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: true, sortDown: false, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: 'avg', @@ -186,7 +200,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). defaultSort: false, // Whether the column is the default sort for the table. should only be true for one column. sortDown: false, // Whether the column should initially sort descending or ascending. - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: 'median', @@ -194,7 +208,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '009', @@ -202,7 +216,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '1019', @@ -210,7 +224,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '2029', @@ -218,7 +232,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '3039', @@ -226,7 +240,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '4049', @@ -234,7 +248,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '5059', @@ -242,7 +256,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '6069', @@ -250,7 +264,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '7079', @@ -258,7 +272,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '8089', @@ -266,7 +280,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '9099', @@ -274,7 +288,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { id: '100', @@ -282,7 +296,7 @@ export class AdminGradesTab extends AdminPage { sortable: true, defaultSort: false, sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' } ]; diff --git a/packages/portal/frontend/src/app/views/AdminResultsTab.ts b/packages/portal/frontend/src/app/views/AdminResultsTab.ts index 916e8d4eb..64bc7d594 100644 --- a/packages/portal/frontend/src/app/views/AdminResultsTab.ts +++ b/packages/portal/frontend/src/app/views/AdminResultsTab.ts @@ -48,14 +48,14 @@ export class AdminResultsTab extends AdminPage { UI.hideModal(); const fab = document.querySelector('#resultsUpdateButton') as OnsButtonElement; - fab.onclick = function(evt: any) { + fab.onclick = function (evt: any) { Log.info('AdminResultsTab::init(..)::updateButton::onClick'); UI.showModal('Retrieving results.'); - that.performQueries().then(function(newResults) { + that.performQueries().then(function (newResults) { // TODO: need to track and update the current value of deliv and repo that.render(delivs, repos, newResults); UI.hideModal(); - }).catch(function(err) { + }).catch(function (err) { UI.showError(err); }); }; @@ -105,52 +105,52 @@ export class AdminResultsTab extends AdminPage { const headers: TableHeader[] = [ { - id: '?', - text: '?', - sortable: false, + id: '?', + text: '?', + sortable: false, defaultSort: false, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { - id: 'repoId', - text: 'Repository', - sortable: true, + id: 'repoId', + text: 'Repository', + sortable: true, defaultSort: false, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: left;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em; text-align: left;' }, { - id: 'delivId', - text: 'Deliv', - sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). + id: 'delivId', + text: 'Deliv', + sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). defaultSort: false, // Whether the column is the default sort for the table. should only be true for one column. - sortDown: false, // Whether the column should initially sort descending or ascending. - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + sortDown: false, // Whether the column should initially sort descending or ascending. + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { - id: 'score', - text: 'Score', - sortable: true, + id: 'score', + text: 'Score', + sortable: true, defaultSort: false, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { - id: 'state', - text: 'State', - sortable: true, + id: 'state', + text: 'State', + sortable: true, defaultSort: false, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' }, { - id: 'timstamp', - text: 'Timestamp', - sortable: true, + id: 'timstamp', + text: 'Timestamp', + sortable: true, defaultSort: true, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em; text-align: center;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em; text-align: center;' } ]; @@ -179,16 +179,34 @@ export class AdminResultsTab extends AdminPage { const stdioViewerURL = '/stdio.html?delivId=' + result.delivId + '&repoId=' + result.repoId + '&sha=' + result.commitSHA; + let score: number | string = ''; + score = result.scoreOverall; + if (score === 100) { + score = "100.00"; + } else { + // two decimal places + score = score.toFixed(2); + // prepend space (not 100) + score = " " + score; + if (result.scoreOverall < 10) { + // prepend with extra space if < 10 + score = " " + score; + } + } + const row: TableCell[] = [ { value: '', - html: '' + html: '' + }, + { + value: result.repoId, + html: '' + result.repoId + '' }, - {value: result.repoId, html: '' + result.repoId + ''}, // {value: result.repoId, html: result.repoId}, {value: result.delivId, html: result.delivId}, - {value: result.scoreOverall, html: result.scoreOverall + ''}, + {value: result.scoreOverall, html: score}, {value: result.state, html: result.state}, {value: ts, html: '' + tsString + ''} ]; From 8c4b1004aba0fbd5f7939764c239438a5f572b66 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 21:02:36 -0700 Subject: [PATCH 015/104] add chai-as-promised --- packages/portal/backend/package.json | 3 ++ packages/portal/backend/test/FactorySpec.ts | 47 +++++++++------------ yarn.lock | 47 +++++++++------------ 3 files changed, 44 insertions(+), 53 deletions(-) diff --git a/packages/portal/backend/package.json b/packages/portal/backend/package.json index 52dc6071d..ea5d38d67 100644 --- a/packages/portal/backend/package.json +++ b/packages/portal/backend/package.json @@ -24,10 +24,13 @@ "homepage": "https://github.com/ubccpsc/sdmm-portal-backend#readme", "dependencies": { "@babel/core": "^7.0.0-0", + "@types/chai": "^4.3.3", + "@types/chai-as-promised": "^7.1.5", "@types/cookie": "^0.3.1", "@types/node": "^12.0.0", "@types/supertest": "^2.0.8", "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", "child-process-promise": "^2.2.1", "client-oauth2": "^4.2.1", "codecov": "^3.0.4", diff --git a/packages/portal/backend/test/FactorySpec.ts b/packages/portal/backend/test/FactorySpec.ts index 3bbcd1bbd..8194dff32 100644 --- a/packages/portal/backend/test/FactorySpec.ts +++ b/packages/portal/backend/test/FactorySpec.ts @@ -1,17 +1,22 @@ -import {expect} from "chai"; +// import { use as chaiUse } from 'chai'; +import {expect, use as chaiUse} from "chai"; +import * as chaiAsPromised from "chai-as-promised"; import "mocha"; import Log from "../../../common/Log"; import {Factory} from "../src/Factory"; import './GlobalSpec'; +import Config, {ConfigKey} from "@common/Config"; -describe('Factory', function() { +chaiUse(chaiAsPromised); + +describe('Factory', function () { /** * These are all terrible tests and just make sure that _some_ object is returned. */ - it('Can get the route handler for courses', async function() { + it('Can get the route handler for courses', async function () { let actual = Factory.getCustomRouteHandler('classytest'); expect(actual).to.not.be.null; @@ -35,36 +40,26 @@ describe('Factory', function() { expect(ex).to.be.null; }); - it('Can get the course controller for courses', async function() { + it('Can get the course controller for courses', async function () { // should be able to get our test controller const actual = await Factory.getCourseController(null, 'classytest'); Log.test("Controller should not be null: " + actual); expect(actual).to.not.be.null; + }); - // NOTE: this behaviour is different now: we just return the CustomCourseController no matter what. + it('Invalid plugins should be handled gracefully', async function () { + const pluginVal = Config.getInstance().getProp(ConfigKey.plugin); + Config.getInstance().setProp(ConfigKey.plugin, "INVALIDPLUGIN"); - // should fail to get a controller for a course that doesn't exist - // actual = null; - // let ex = null; - // try { - // actual = await Factory.getCourseController(null, 'INVALIDcourseNAME'); - // Log.test("Controller should be null: " + actual); - // } catch (err) { - // ex = err; - // } - // expect(actual).to.be.null; - // expect(ex).to.not.be.null; + Log.test("1"); + await expect(Factory.getCustomRouteHandler('INVALID_PLUGIN')).to.eventually.throw; + Log.test("2"); + await expect(Factory.getCourseController(null, 'INVALID_PLUGIN')).to.eventually.throw; + Log.test("3"); + await expect(Factory.getCourseController(undefined, undefined)).to.eventually.throw; + Log.test("4"); - // actual = null; - // ex = null; - // try { - // // won't error because it uses the default name - // actual = Factory.getCourseController(); - // } catch (err) { - // ex = err; - // } - // expect(actual).to.be.null; - // expect(ex).to.not.be.null; + Config.getInstance().setProp(ConfigKey.plugin, pluginVal); }); }); diff --git a/yarn.lock b/yarn.lock index b46069332..30f8bb120 100644 --- a/yarn.lock +++ b/yarn.lock @@ -331,6 +331,18 @@ dependencies: "@types/node" "*" +"@types/chai-as-promised@^7.1.5": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz#6e016811f6c7a64f2eed823191c3a6955094e255" + integrity sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ== + dependencies: + "@types/chai" "*" + +"@types/chai@*", "@types/chai@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.3.tgz#3c90752792660c4b562ad73b3fbd68bf3bc7ae07" + integrity sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g== + "@types/chai@4.0.8": version "4.0.8" resolved "https://registry.nlark.com/@types/chai/download/@types/chai-4.0.8.tgz#d27600e9ba2f371e08695d90a0fe0408d89c7be7" @@ -1075,6 +1087,13 @@ caseless@~0.12.0: resolved "https://registry.npm.taobao.org/caseless/download/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chai-as-promised@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" + integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA== + dependencies: + check-error "^1.0.2" + chai@4.1.2: version "4.1.2" resolved "https://registry.npm.taobao.org/chai/download/chai-4.1.2.tgz?cache=0&sync_timestamp=1615567871070&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchai%2Fdownload%2Fchai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" @@ -2811,13 +2830,6 @@ make-error@^1.1.1, make-error@^1.3.5: resolved "https://registry.npm.taobao.org/make-error/download/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha1-LrLjfqm2fEiR9oShOUeZr0hM96I= -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - markdown-table@^1.1.2: version "1.1.3" resolved "https://registry.nlark.com/markdown-table/download/markdown-table-1.1.3.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fmarkdown-table%2Fdownload%2Fmarkdown-table-1.1.3.tgz#9fcb69bcfdb8717bfd0398c6ec2d93036ef8de60" @@ -2837,15 +2849,6 @@ media-typer@0.3.0: resolved "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.npm.taobao.org/memory-pager/download/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" @@ -2911,7 +2914,7 @@ mime@^2.4.3, mime@^2.4.4: resolved "https://registry.npm.taobao.org/mime/download/mime-2.5.2.tgz?cache=0&sync_timestamp=1613584838235&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha1-bj3GzCuVEGQ4MOXxnVy3U9pe6r4= -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -3243,16 +3246,6 @@ optional-require@^1.0.3: dependencies: require-at "^1.0.6" -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw== - -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.nlark.com/p-limit/download/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" From 530ea1811a7a43bc93421eac102b0c70df7444da Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 21:31:18 -0700 Subject: [PATCH 016/104] try to get mocha building in CI again --- package.json | 5 +- packages/portal/backend/package.json | 8 +- packages/portal/backend/test/FactorySpec.ts | 4 +- yarn.lock | 150 ++++++++++++++++---- 4 files changed, 132 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index d28d7458c..c7a22749f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "packages/portal/backend" ], "devDependencies": { - "@types/chai": "4.0.8", "@types/dotenv": "4.0.2", "@types/fs-extra": "5.0.0", "@types/jszip": "3.1.6", @@ -34,9 +33,9 @@ "@types/node": "~8.10.53", "@types/node-fetch": "^2.5.5", "@types/restify": "^8.4.1", - "chai": "4.1.2", + "chai": "^4.3.6", "jsonschema": "1.2.2", - "mocha": "^9.0.0", + "mocha": "^9.2.2", "ts-node": "4.1.0", "tslint": "^5.11.0", "typescript": "^3.7.2", diff --git a/packages/portal/backend/package.json b/packages/portal/backend/package.json index ea5d38d67..abe57238a 100644 --- a/packages/portal/backend/package.json +++ b/packages/portal/backend/package.json @@ -24,12 +24,12 @@ "homepage": "https://github.com/ubccpsc/sdmm-portal-backend#readme", "dependencies": { "@babel/core": "^7.0.0-0", - "@types/chai": "^4.3.3", - "@types/chai-as-promised": "^7.1.5", "@types/cookie": "^0.3.1", "@types/node": "^12.0.0", "@types/supertest": "^2.0.8", - "chai": "^4.1.2", + "@types/chai": "^4.3.3", + "@types/chai-as-promised": "^7.1.5", + "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "child-process-promise": "^2.2.1", "client-oauth2": "^4.2.1", @@ -41,7 +41,7 @@ "dotenv": "^5.0.1", "fs-extra": "^5.0.0", "markdown-table": "^1.1.2", - "mocha": "^9.0.0", + "mocha": "^9.2.2", "mocha-junit-reporter": "^1.17.0", "mongodb": "^3.0.3", "node-schedule": "^1.3.0", diff --git a/packages/portal/backend/test/FactorySpec.ts b/packages/portal/backend/test/FactorySpec.ts index 8194dff32..f2e42cce9 100644 --- a/packages/portal/backend/test/FactorySpec.ts +++ b/packages/portal/backend/test/FactorySpec.ts @@ -1,9 +1,9 @@ // import { use as chaiUse } from 'chai'; -import {expect, use as chaiUse} from "chai"; +import {use as chaiUse, expect} from "chai"; import * as chaiAsPromised from "chai-as-promised"; import "mocha"; -import Log from "../../../common/Log"; +import Log from "../../../common/Log"; import {Factory} from "../src/Factory"; import './GlobalSpec'; diff --git a/yarn.lock b/yarn.lock index 30f8bb120..68a509a43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -343,11 +343,6 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.3.tgz#3c90752792660c4b562ad73b3fbd68bf3bc7ae07" integrity sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g== -"@types/chai@4.0.8": - version "4.0.8" - resolved "https://registry.nlark.com/@types/chai/download/@types/chai-4.0.8.tgz#d27600e9ba2f371e08695d90a0fe0408d89c7be7" - integrity sha1-0nYA6bovNx4IaV2QoP4ECNice+c= - "@types/component-emitter@^1.2.10": version "1.2.10" resolved "https://registry.nlark.com/@types/component-emitter/download/@types/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" @@ -826,7 +821,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.npm.taobao.org/assert-plus/download/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assertion-error@^1.0.1, assertion-error@^1.1.0: +assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.npm.taobao.org/assertion-error/download/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs= @@ -1094,27 +1089,16 @@ chai-as-promised@^7.1.1: dependencies: check-error "^1.0.2" -chai@4.1.2: - version "4.1.2" - resolved "https://registry.npm.taobao.org/chai/download/chai-4.1.2.tgz?cache=0&sync_timestamp=1615567871070&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchai%2Fdownload%2Fchai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" - integrity sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw= - dependencies: - assertion-error "^1.0.1" - check-error "^1.0.1" - deep-eql "^3.0.0" - get-func-name "^2.0.0" - pathval "^1.0.0" - type-detect "^4.0.0" - -chai@^4.1.2: - version "4.3.4" - resolved "https://registry.npm.taobao.org/chai/download/chai-4.3.4.tgz?cache=0&sync_timestamp=1615567871070&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchai%2Fdownload%2Fchai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" - integrity sha1-tV5lWzHh6scJm+TAjCGWT84ubEk= +chai@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" + integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" deep-eql "^3.0.1" get-func-name "^2.0.0" + loupe "^2.3.1" pathval "^1.1.1" type-detect "^4.0.5" @@ -1140,7 +1124,7 @@ charenc@0.0.2: resolved "https://registry.npm.taobao.org/charenc/download/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= -check-error@^1.0.1, check-error@^1.0.2: +check-error@^1.0.2: version "1.0.2" resolved "https://registry.npm.taobao.org/check-error/download/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= @@ -1169,6 +1153,21 @@ chokidar@3.5.2, chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.2" +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^1.0.1: version "1.1.4" resolved "https://registry.nlark.com/chownr/download/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -1489,6 +1488,13 @@ debug@4.3.1: dependencies: ms "2.1.2" +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@^3.2.6: version "3.2.7" resolved "https://registry.nlark.com/debug/download/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -1506,7 +1512,7 @@ decamelize@^4.0.0: resolved "https://registry.npm.taobao.org/decamelize/download/decamelize-4.0.0.tgz?cache=0&sync_timestamp=1610348638646&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdecamelize%2Fdownload%2Fdecamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha1-qkcte/Zg6xXzSU79UxyrfypwmDc= -deep-eql@^3.0.0, deep-eql@^3.0.1: +deep-eql@^3.0.1: version "3.0.1" resolved "https://registry.npm.taobao.org/deep-eql/download/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" integrity sha1-38lARACtHI/gI+faHfHBR8S0RN8= @@ -2116,6 +2122,18 @@ glob@7.1.7, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^6.0.1: version "6.0.4" resolved "https://registry.nlark.com/glob/download/glob-6.0.4.tgz?cache=0&sync_timestamp=1620337555606&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fglob%2Fdownload%2Fglob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -2782,6 +2800,13 @@ long-timeout@0.1.1: resolved "https://registry.npm.taobao.org/long-timeout/download/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" integrity sha1-lyHXiLR+C8taJMLivuGg2lXatRQ= +loupe@^2.3.1: + version "2.3.4" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" + integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== + dependencies: + get-func-name "^2.0.0" + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.5.tgz?cache=0&sync_timestamp=1594427606170&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flru-cache%2Fdownload%2Flru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -2830,6 +2855,13 @@ make-error@^1.1.1, make-error@^1.3.5: resolved "https://registry.npm.taobao.org/make-error/download/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha1-LrLjfqm2fEiR9oShOUeZr0hM96I= +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + markdown-table@^1.1.2: version "1.1.3" resolved "https://registry.nlark.com/markdown-table/download/markdown-table-1.1.3.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fmarkdown-table%2Fdownload%2Fmarkdown-table-1.1.3.tgz#9fcb69bcfdb8717bfd0398c6ec2d93036ef8de60" @@ -2849,6 +2881,15 @@ media-typer@0.3.0: resolved "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.npm.taobao.org/memory-pager/download/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" @@ -2914,7 +2955,7 @@ mime@^2.4.3, mime@^2.4.4: resolved "https://registry.npm.taobao.org/mime/download/mime-2.5.2.tgz?cache=0&sync_timestamp=1613584838235&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha1-bj3GzCuVEGQ4MOXxnVy3U9pe6r4= -mimic-fn@^2.1.0: +mimic-fn@^2.0.0, mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -2931,6 +2972,13 @@ minimalistic-assert@^1.0.0: dependencies: brace-expansion "^1.1.7" +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.2.0, minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -3031,6 +3079,36 @@ mocha@^9.0.0: yargs-parser "20.2.4" yargs-unparser "2.0.0" +mocha@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + moment-timezone@^0.5.31: version "0.5.37" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.37.tgz#adf97f719c4e458fdb12e2b4e87b8bec9f4eef1e" @@ -3095,6 +3173,11 @@ nanoid@3.1.23: resolved "https://registry.nlark.com/nanoid/download/nanoid-3.1.23.tgz?cache=0&sync_timestamp=1628771925127&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fnanoid%2Fdownload%2Fnanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" integrity sha1-90QIbOfCvEfuCoRyV01ceOQYOoE= +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + ncp@~2.0.0: version "2.0.0" resolved "https://registry.npm.taobao.org/ncp/download/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" @@ -3246,6 +3329,16 @@ optional-require@^1.0.3: dependencies: require-at "^1.0.6" +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw== + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.nlark.com/p-limit/download/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -3348,7 +3441,7 @@ path-type@^4.0.0: resolved "https://registry.npm.taobao.org/path-type/download/path-type-4.0.0.tgz?cache=0&sync_timestamp=1611752058913&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpath-type%2Fdownload%2Fpath-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha1-hO0BwKe6OAr+CdkKjBgNzZ0DBDs= -pathval@^1.0.0, pathval@^1.1.1: +pathval@^1.1.1: version "1.1.1" resolved "https://registry.npm.taobao.org/pathval/download/pathval-1.1.1.tgz?cache=0&sync_timestamp=1611662316573&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpathval%2Fdownload%2Fpathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha1-hTTnenfOesWiUS6iHg/bj89sPY0= @@ -4755,6 +4848,11 @@ workerpool@6.1.5: resolved "https://registry.nlark.com/workerpool/download/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581" integrity sha1-D3zwdrYhX9fh2pA/9vIt3RiGtYE= +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-6.2.0.tgz?cache=0&sync_timestamp=1618558850700&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwrap-ansi%2Fdownload%2Fwrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" From daad5fe00292e85296f374468b702654e01a414e Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 21:51:31 -0700 Subject: [PATCH 017/104] This test is destined to be a flaky nightmare, but one more try! --- packages/autotest/test/GitHubAutoTestSpec.ts | 10 +++++++++- packages/common/Util.ts | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/autotest/test/GitHubAutoTestSpec.ts b/packages/autotest/test/GitHubAutoTestSpec.ts index 3c9924755..7c8f32b10 100644 --- a/packages/autotest/test/GitHubAutoTestSpec.ts +++ b/packages/autotest/test/GitHubAutoTestSpec.ts @@ -133,6 +133,11 @@ describe("GitHubAutoTest", () => { it("Rapid requests should go to the regression queue.", async () => { expect(at).to.not.be.null; + await Util.delay(WAIT); // let old tests expire before starting + + // start fresh + await data.clearData(); + stubDependencies(); let allData = await data.getAllData(); expect(allData.pushes.length).to.equal(0); @@ -142,6 +147,7 @@ describe("GitHubAutoTest", () => { await at.handlePushEvent(pushes[3]); await at.handlePushEvent(pushes[4]); await at.handlePushEvent(pushes[5]); + Log.test("all pushes sent"); // to see at what admin pushes look like // const push = Object.assign({}, pushes[5]); @@ -159,13 +165,15 @@ describe("GitHubAutoTest", () => { const eq = (at["expressQueue"] as any); const sq = (at["standardQueue"] as any); const rq = (at["regressionQueue"] as any); + Log.test("about to check values"); expect(eq.slots).to.have.length(0); // nothing should be running on express expect(eq.data).to.have.length(0); // nothing should be queued on express expect(sq.slots).to.have.length(2); // two should be running on standard expect(sq.data).to.have.length(2); // two should be waiting on standard expect(rq.slots).to.have.length(1); // one should be running on regression expect(rq.data).to.have.length(1); // one should be queued on regression - }).timeout(WAIT * 2); + Log.test("values checked"); + }).timeout(WAIT * 3); it("Should gracefully fail with bad comments.", async () => { expect(at).not.to.equal(null); diff --git a/packages/common/Util.ts b/packages/common/Util.ts index 5c852ee7f..23abe880b 100644 --- a/packages/common/Util.ts +++ b/packages/common/Util.ts @@ -77,7 +77,7 @@ export default class Util { const actual = Date.now(); const took = actual - start; const delta = actual - fire.getTime(); - Log.info("Util::delay( " + ms + " ms ) - fired; took: " + took + " ms; jitter: " + delta + " ms"); + Log.trace("Util::delay( " + ms + " ms ) - fired; took: " + took + " ms; jitter: " + delta + " ms"); resolve(); }, ms); }); From 2bde72555606d3ba4141e78db0eb32bd25769a84 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Tue, 20 Sep 2022 22:29:27 -0700 Subject: [PATCH 018/104] test cleaning --- packages/portal/backend/test/CSVParserSpec.ts | 29 +++++++ .../backend/test/data/gradesInconsistent.csv | 4 + .../backend/test/data/gradesInconsistent2.csv | 4 + .../backend/test/server/AdminRoutesSpec.ts | 75 ++++++++++++++----- 4 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 packages/portal/backend/test/data/gradesInconsistent.csv create mode 100644 packages/portal/backend/test/data/gradesInconsistent2.csv diff --git a/packages/portal/backend/test/CSVParserSpec.ts b/packages/portal/backend/test/CSVParserSpec.ts index 04f6b01df..478476ecb 100644 --- a/packages/portal/backend/test/CSVParserSpec.ts +++ b/packages/portal/backend/test/CSVParserSpec.ts @@ -76,4 +76,33 @@ describe('CSVParser', function() { expect(rows).to.be.null; expect(ex).to.not.be.null; }); + + it('Handle CSVs with inconsistent person IDs (GitHub)', async function() { + let rows = null; + let ex = null; + try { + const path = __dirname + '/data/gradesInconsistent.csv'; + const csv = new CSVParser(); + rows = await csv.processGrades(Test.ADMIN1.id, Test.DELIVID1, path); + } catch (err) { + ex = err; + } + expect(rows).to.be.null; + expect(ex).to.not.be.null; + }); + + it('Handle CSVs with inconsistent person IDs (CWL)', async function() { + let rows = null; + let ex = null; + try { + const path = __dirname + '/data/gradesInconsistent2.csv'; + const csv = new CSVParser(); + rows = await csv.processGrades(Test.ADMIN1.id, Test.DELIVID1, path); + } catch (err) { + ex = err; + } + expect(rows).to.be.null; + expect(ex).to.not.be.null; + }); + }); diff --git a/packages/portal/backend/test/data/gradesInconsistent.csv b/packages/portal/backend/test/data/gradesInconsistent.csv new file mode 100644 index 000000000..569724b9a --- /dev/null +++ b/packages/portal/backend/test/data/gradesInconsistent.csv @@ -0,0 +1,4 @@ +GITHUB,GRADE,COMMENT +user1ID,92,csv comment +user2ID,29,csv comment +user3ID,19,csv comment diff --git a/packages/portal/backend/test/data/gradesInconsistent2.csv b/packages/portal/backend/test/data/gradesInconsistent2.csv new file mode 100644 index 000000000..a697711c1 --- /dev/null +++ b/packages/portal/backend/test/data/gradesInconsistent2.csv @@ -0,0 +1,4 @@ +CWL,GRADE,COMMENT +user1ID,92,csv comment +user2ID,29,csv comment +user3ID,19,csv comment diff --git a/packages/portal/backend/test/server/AdminRoutesSpec.ts b/packages/portal/backend/test/server/AdminRoutesSpec.ts index 374e836b4..aa0238411 100644 --- a/packages/portal/backend/test/server/AdminRoutesSpec.ts +++ b/packages/portal/backend/test/server/AdminRoutesSpec.ts @@ -3,10 +3,10 @@ import "mocha"; import * as restify from 'restify'; import * as request from 'supertest'; -import Config, {ConfigKey} from "../../../../common/Config"; -import Log from "../../../../common/Log"; +import Config, {ConfigKey} from "@common/Config"; +import Log from "@common/Log"; -import {Test} from "../../../../common/TestHarness"; +import {Test} from "@common/TestHarness"; import { AutoTestConfigTransport, AutoTestResultPayload, @@ -19,14 +19,14 @@ import { StudentTransportPayload, TeamFormationTransport, TeamTransportPayload -} from "../../../../common/types/PortalTypes"; -import Util from "../../../../common/Util"; -import {DatabaseController} from "../../src/controllers/DatabaseController"; -import {DeliverablesController} from "../../src/controllers/DeliverablesController"; -import {GitHubActions} from "../../src/controllers/GitHubActions"; -import {PersonController} from "../../src/controllers/PersonController"; -import {TeamController} from "../../src/controllers/TeamController"; -import BackendServer from "../../src/server/BackendServer"; +} from "@common/types/PortalTypes"; +import Util from "@common/Util"; +import {DatabaseController} from "@backend/controllers/DatabaseController"; +import {DeliverablesController} from "@backend/controllers/DeliverablesController"; +import {GitHubActions} from "@backend/controllers/GitHubActions"; +import {PersonController} from "@backend/controllers/PersonController"; +import {TeamController} from "@backend/controllers/TeamController"; +import BackendServer from "@backend/server/BackendServer"; import './AuthRoutesSpec'; describe('Admin Routes', function () { @@ -107,7 +107,7 @@ describe('Admin Routes', function () { // should confirm body.success objects (at least one) }).timeout(Test.TIMEOUT); - it('Should not be able to get a list of students if the requestor is not privileged', async function () { + it('Should not be able to get a list of students if the requester is not privileged', async function () { let response = null; let body: StudentTransportPayload; @@ -177,7 +177,7 @@ describe('Admin Routes', function () { // should confirm body.success objects (at least one) }); - it('Should not be able to get a list of teams if the requestor is not privileged', async function () { + it('Should not be able to get a list of teams if the requester is not privileged', async function () { let response = null; let body: StudentTransportPayload; @@ -194,8 +194,7 @@ describe('Admin Routes', function () { expect(body.failure).to.not.be.undefined; }); - it('Should be able to get a list of students', async function () { - + it('Should be able to get a list of grades', async function () { let response = null; let body: StudentTransportPayload; const url = '/portal/admin/grades'; @@ -213,7 +212,7 @@ describe('Admin Routes', function () { // should confirm body.success objects (at least one) }).timeout(Test.TIMEOUT); - it('Should not be able to get a list of grades if the requestor is not privileged', async function () { + it('Should not be able to get a list of grades if the requester is not privileged', async function () { let response = null; let body: StudentTransportPayload; @@ -230,6 +229,44 @@ describe('Admin Routes', function () { expect(body.failure).to.not.be.undefined; }); + it('Should be able to get a list of graded results for a deliverable', async function () { + let response = null; + let body: StudentTransportPayload; + + const url = '/portal/admin/gradedResults/d0'; + try { + response = await request(app).get(url).set({user: userName, token: userToken}); + body = response.body; + } catch (err) { + Log.test('ERROR: ' + err); + } + Log.test(response.status + " -> " + JSON.stringify(body)); + expect(response.status).to.equal(200); + expect(body.success).to.not.be.undefined; + expect(body.success).to.be.an('array'); + + // should confirm body.success objects (at least one) + }).timeout(Test.TIMEOUT); + + it('Should be able to get a list of the best graded results for a deliverable', async function () { + let response = null; + let body: StudentTransportPayload; + + const url = '/portal/admin/bestResults/d0'; + try { + response = await request(app).get(url).set({user: userName, token: userToken}); + body = response.body; + } catch (err) { + Log.test('ERROR: ' + err); + } + Log.test(response.status + " -> " + JSON.stringify(body)); + expect(response.status).to.equal(200); + expect(body.success).to.not.be.undefined; + expect(body.success).to.be.an('array'); + + // should confirm body.success objects (at least one) + }).timeout(Test.TIMEOUT); + it('Should be able to get a list of results', async function () { let response = null; @@ -250,7 +287,7 @@ describe('Admin Routes', function () { // should confirm body.success objects (at least one) }); - it('Should not be able to get a list of results if the requestor is not privileged', async function () { + it('Should not be able to get a list of results if the requester is not privileged', async function () { let response = null; let body: AutoTestResultPayload; @@ -287,7 +324,7 @@ describe('Admin Routes', function () { // should confirm body.success objects (at least one) }); - it('Should not be able to get a list of dashboard results if the requestor is not privileged', async function () { + it('Should not be able to get a list of dashboard results if the requester is not privileged', async function () { let response = null; let body: AutoTestResultPayload; @@ -324,7 +361,7 @@ describe('Admin Routes', function () { // should confirm body.success objects (at least one) }); - it('Should not be able to export the list of dashboard results if the requestor is not privileged', async function () { + it('Should not be able to export the list of dashboard results if the requester is not privileged', async function () { let response = null; let body: AutoTestResultPayload; From 9e0c3d6584a8d1835d57e06e9d2e7bd48292f879 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Wed, 21 Sep 2022 08:41:45 -0700 Subject: [PATCH 019/104] allow LOG_LEVEL to be set in .env decrease info logging verbosity so it can be more easily traced in prod --- packages/common/Config.ts | 92 ++++++++++--------- packages/common/Log.ts | 80 +++++++++++----- .../src/controllers/AdminController.ts | 12 +-- .../src/controllers/CourseController.ts | 10 +- .../src/controllers/DatabaseController.ts | 2 +- .../src/controllers/DeliverablesController.ts | 49 +++++----- .../src/controllers/GradesController.ts | 12 +-- .../src/controllers/RepositoryController.ts | 39 ++++---- .../backend/src/controllers/TeamController.ts | 25 ++--- .../backend/src/server/common/AuthRoutes.ts | 70 +++++++------- .../src/server/common/GeneralRoutes.ts | 16 ++-- 11 files changed, 233 insertions(+), 174 deletions(-) diff --git a/packages/common/Config.ts b/packages/common/Config.ts index 6ab253b2a..65f832b30 100644 --- a/packages/common/Config.ts +++ b/packages/common/Config.ts @@ -1,11 +1,11 @@ import * as dotenv from "dotenv"; import Log, {LogLevel} from "./Log"; -// const dotenv = require('dotenv'); -const result = dotenv.config({path: __dirname + '/../../.env'}); -if (result.error) { - Log.error("Failed to parse .env " + result.error); - throw result.error; +const envLoadResult = dotenv.config({path: __dirname + '/../../.env'}); + +if (envLoadResult.error) { + Log.error("Failed to parse .env " + envLoadResult.error); + throw envLoadResult.error; } /** @@ -79,9 +79,13 @@ export enum ConfigKey { adminTeamName = "adminTeamName", // STAFF_TEAM_NAME staffTeamName = "staffTeamName", + + logLevel = "logLevel", } export default class Config { + protected static instance: Config = null; + private config: any; public static getInstance(): Config { if (Config.instance === null) { @@ -91,29 +95,25 @@ export default class Config { return Config.instance; } - protected static instance: Config = null; - - private config: any; - private constructor() { // should not be called by clients but typescript does not allow private constructors try { this.config = { - name: process.env.NAME, - org: process.env.ORG, - testorg: process.env.ORGTEST, + name: process.env.NAME, + org: process.env.ORG, + testorg: process.env.ORGTEST, testname: process.env.NAMETEST, plugin: process.env.PLUGIN, - classlist_uri: process.env.CLASSLIST_URI, + classlist_uri: process.env.CLASSLIST_URI, classlist_username: process.env.CLASSLIST_USERNAME, classlist_password: process.env.CLASSLIST_PASSWORD, minimum_student_delay: process.env.MINIMUM_STUDENT_DELAY, publichostname: process.env.PUBLICHOSTNAME, - hostDir: process.env.HOST_DIR, - postback: Boolean(process.env.AUTOTEST_POSTBACK), + hostDir: process.env.HOST_DIR, + postback: Boolean(process.env.AUTOTEST_POSTBACK), persistDir: process.env.PERSIST_DIR, dockerUid: process.env.UID, hostsAllow: process.env.HOSTS_ALLOW, @@ -121,50 +121,60 @@ export default class Config { timeout: Number(process.env.GRADER_TIMEOUT), botName: process.env.GH_BOT_USERNAME, - sslCertPath: process.env.SSL_CERT_PATH, - sslKeyPath: process.env.SSL_KEY_PATH, + sslCertPath: process.env.SSL_CERT_PATH, + sslKeyPath: process.env.SSL_KEY_PATH, mongoUrl: process.env.DB_URL, - backendPort: process.env.BACKEND_PORT, - backendUrl: process.env.BACKEND_URL, + backendPort: process.env.BACKEND_PORT, + backendUrl: process.env.BACKEND_URL, - githubHost: process.env.GH_HOST, - githubAPI: process.env.GH_API, - githubBotName: process.env.GH_BOT_USERNAME, - githubBotToken: process.env.GH_BOT_TOKEN, - githubClientId: process.env.GH_CLIENT_ID, + githubHost: process.env.GH_HOST, + githubAPI: process.env.GH_API, + githubBotName: process.env.GH_BOT_USERNAME, + githubBotToken: process.env.GH_BOT_TOKEN, + githubClientId: process.env.GH_CLIENT_ID, githubClientSecret: process.env.GH_CLIENT_SECRET, - githubDockerToken: process.env.GH_DOCKER_TOKEN, + githubDockerToken: process.env.GH_DOCKER_TOKEN, - githubAdmin: process.env.GH_ADMIN, - githubAdminStaff: process.env.GH_ADMIN_STAFF, - githubStaff: process.env.GH_STAFF, - githubBot01: process.env.GH_BOT_01, - githubBot02: process.env.GH_BOT_02, - githubTestUsers: process.env.GH_TEST_USERS, + githubAdmin: process.env.GH_ADMIN, + githubAdminStaff: process.env.GH_ADMIN_STAFF, + githubStaff: process.env.GH_STAFF, + githubBot01: process.env.GH_BOT_01, + githubBot02: process.env.GH_BOT_02, + githubTestUsers: process.env.GH_TEST_USERS, - autotestUrl: process.env.AUTOTEST_URL, - autotestPort: process.env.AUTOTEST_PORT, + autotestUrl: process.env.AUTOTEST_URL, + autotestPort: process.env.AUTOTEST_PORT, autotestSecret: process.env.AUTOTEST_SECRET, - patchId: process.env.PATCH_ID, - patchToolUrl: process.env.PATCH_TOOL_URL, + patchId: process.env.PATCH_ID, + patchToolUrl: process.env.PATCH_TOOL_URL, patchSourceRepo: process.env.PATCH_SOURCE_REPO, - adminTeamName: process.env.ADMIN_TEAM_NAME, - staffTeamName: process.env.STAFF_TEAM_NAME, + adminTeamName: process.env.ADMIN_TEAM_NAME, + staffTeamName: process.env.STAFF_TEAM_NAME, + + logLevel: process.env.LOG_LEVEL }; - // this is not a great place for this - // but at least it should happen near the start of any execution Log.info("Config - Log::"); + + // This is not a great place to sniff for the CI environment + // but at least it should happen near the start of any execution. const ci = process.env.CI; if (typeof ci !== 'undefined' && Boolean(ci) === true) { + // CI instances should be INFO always + // trace emits too much text so the CI buffer does not save it all Log.info("Config - Log:: - CI detected; changing to INFO"); - Log.Level = LogLevel.INFO; // change to INFO from TRACE if on CI + Log.Level = LogLevel.INFO; // force INFO } else { - Log.info("Config - Log:: - CI NOT detected"); + const level = this.getProp(ConfigKey.logLevel); + if (typeof this.config.logLevel !== "undefined" && level !== null) { + Log.info("Config - Log:: - updating log level to: " + level); + Log.Level = level; + Log.parseLogLevel(); + } } } catch (err) { diff --git a/packages/common/Log.ts b/packages/common/Log.ts index b8aea2f59..2ac96b03b 100644 --- a/packages/common/Log.ts +++ b/packages/common/Log.ts @@ -11,34 +11,63 @@ export enum LogLevel { let LOG_LEVEL: LogLevel; -switch ((process.env["LOG_LEVEL"] || "").toUpperCase()) { - case "TRACE": - LOG_LEVEL = LogLevel.TRACE; - break; - case "INFO": - LOG_LEVEL = LogLevel.INFO; - break; - case "WARN": - LOG_LEVEL = LogLevel.WARN; - break; - case "ERROR": - LOG_LEVEL = LogLevel.ERROR; - break; - case "TEST": - LOG_LEVEL = LogLevel.TEST; - break; - case "NONE": - LOG_LEVEL = LogLevel.NONE; - break; - default: - LOG_LEVEL = LogLevel.TRACE; -} - /** * Collection of logging methods. Useful for making the output easier to read and understand. */ export default class Log { - public static Level: LogLevel = LOG_LEVEL; + public static Level: LogLevel = Log.parseLogLevel(); + + public static parseLogLevel(): LogLevel { + try { + console.log("Log::parseLogLevel() - start; currently: " + Log.Level); + let valToSwitch = null; + if (typeof Log.Level === "undefined") { + // if undefined, use .env; otherwise re-parse value + valToSwitch = (process.env["LOG_LEVEL"] || "").toUpperCase(); + } else { + valToSwitch = Log.Level; + } + + if (typeof valToSwitch !== "string") { + LOG_LEVEL = Log.Level; + console.log("Log::parseLogLevel() - unchanged; current level: " + LOG_LEVEL); + return LOG_LEVEL; + } else { + // if the value isn't a string, it must be a LogLevel already + // so we don't need to parse it again + switch (valToSwitch) { + case "TRACE": + LOG_LEVEL = LogLevel.TRACE; + break; + case "INFO": + LOG_LEVEL = LogLevel.INFO; + break; + case "WARN": + LOG_LEVEL = LogLevel.WARN; + break; + case "ERROR": + LOG_LEVEL = LogLevel.ERROR; + break; + case "TEST": + LOG_LEVEL = LogLevel.TEST; + break; + case "NONE": + LOG_LEVEL = LogLevel.NONE; + break; + default: + LOG_LEVEL = LogLevel.TRACE; + } + console.log("Log::parseLogLevel() - log level: " + LOG_LEVEL); + Log.Level = LOG_LEVEL; + return LOG_LEVEL; + } + } catch (err) { + console.log(" Log::parseLogLevel() - ERROR; setting to TRACE"); + Log.Level = LogLevel.TRACE; + LOG_LEVEL = LogLevel.TRACE; + return LOG_LEVEL; + } + } public static trace(...msg: any[]): void { if (Log.Level <= LogLevel.TRACE) { @@ -80,3 +109,6 @@ export default class Log { } } } + +// enable log level changes to dynamically update +Log.parseLogLevel(); diff --git a/packages/portal/backend/src/controllers/AdminController.ts b/packages/portal/backend/src/controllers/AdminController.ts index 44c41ce06..4ae5d274e 100644 --- a/packages/portal/backend/src/controllers/AdminController.ts +++ b/packages/portal/backend/src/controllers/AdminController.ts @@ -59,12 +59,12 @@ export class AdminController { * @returns {Promise} Whether the new grade was saved */ public async processNewAutoTestGrade(grade: AutoTestGradeTransport): Promise { - Log.info("AdminController::processNewAutoTestGrade( .. ) - start"); + Log.trace("AdminController::processNewAutoTestGrade( .. ) - start"); const cc = await Factory.getCourseController(this.gh); try { - Log.info("AdminController::processNewAutoTestGrade( .. ) - payload: " + JSON.stringify(grade)); + Log.trace("AdminController::processNewAutoTestGrade( .. ) - payload: " + JSON.stringify(grade)); const repo = await this.rc.getRepository(grade.repoId); if (repo === null) { // sanity check @@ -79,7 +79,7 @@ export class AdminController { return false; } - Log.info("AdminController::processNewAutoTestGrade( .. ) - getting deliv"); // NOTE: for hangup debugging + Log.trace("AdminController::processNewAutoTestGrade( .. ) - getting deliv"); // NOTE: for hangup debugging const delivController = new DeliverablesController(); const deliv = await delivController.getDeliverable(grade.delivId); @@ -98,12 +98,12 @@ export class AdminController { custom: grade.custom }; - Log.info("AdminController::processNewAutoTestGrade( .. ) - getting grade for " + personId); // NOTE: for hangup debugging + Log.trace("AdminController::processNewAutoTestGrade( .. ) - getting grade for " + personId); // NOTE: for hangup debugging const existingGrade = await this.gc.getGrade(personId, grade.delivId); - Log.info("AdminController::processNewAutoTestGrade( .. ) - handling grade for " + personId + + Log.trace("AdminController::processNewAutoTestGrade( .. ) - handling grade for " + personId + "; existingGrade: " + existingGrade); // NOTE: for hangup debugging const shouldSave = await cc.handleNewAutoTestGrade(deliv, newGrade, existingGrade); - Log.info("AdminController::processNewAutoTestGrade( .. ) - handled grade for " + personId + + Log.trace("AdminController::processNewAutoTestGrade( .. ) - handled grade for " + personId + "; shouldSave: " + shouldSave); // NOTE: for hangup debugging if (shouldSave === true) { diff --git a/packages/portal/backend/src/controllers/CourseController.ts b/packages/portal/backend/src/controllers/CourseController.ts index 4ea317e42..56625b5f1 100644 --- a/packages/portal/backend/src/controllers/CourseController.ts +++ b/packages/portal/backend/src/controllers/CourseController.ts @@ -128,17 +128,17 @@ export class CourseController implements ICourseController { const LOGPRE = "CourseController::handleNewAutoTestGrade( " + deliv.id + ", " + newGrade.personId + ", " + newGrade.score + ", ... ) - URL: " + newGrade.URL + " - "; - Log.info(LOGPRE + "start"); + Log.trace(LOGPRE + "start"); if (newGrade.timestamp < deliv.openTimestamp) { // too early - Log.info(LOGPRE + "not recorded; deliverable not yet open"); + Log.trace(LOGPRE + "not recorded; deliverable not yet open"); return Promise.resolve(false); } if (newGrade.timestamp > deliv.closeTimestamp) { // too late - Log.info(LOGPRE + "not recorded; deliverable closed"); + Log.trace(LOGPRE + "not recorded; deliverable closed"); return Promise.resolve(false); } @@ -159,7 +159,7 @@ export class CourseController implements ICourseController { throw new Error("CourseController::computeNames( ... ) - null Deliverable"); } - Log.info('CourseController::computeNames( ' + deliv.id + ', ... ) - start'); + Log.trace('CourseController::computeNames( ' + deliv.id + ', ... ) - start'); if (people.length < 1) { throw new Error("CourseController::computeNames( ... ) - must provide people"); } @@ -196,7 +196,7 @@ export class CourseController implements ICourseController { const repo = await db.getRepository(rName); if (team === null && repo === null) { - Log.info('CourseController::computeNames( ... ) - done; t: ' + tName); // + ', r: ' + rName); + Log.trace('CourseController::computeNames( ... ) - done; t: ' + tName); // + ', r: ' + rName); return {teamName: tName, repoName: rName}; // return tName; } else { diff --git a/packages/portal/backend/src/controllers/DatabaseController.ts b/packages/portal/backend/src/controllers/DatabaseController.ts index 29454e2c8..2e9d5f3bd 100644 --- a/packages/portal/backend/src/controllers/DatabaseController.ts +++ b/packages/portal/backend/src/controllers/DatabaseController.ts @@ -320,7 +320,7 @@ export class DatabaseController { * @returns {Promise} */ public async writeResult(record: Result): Promise { - Log.info("DatabaseController::writeResult(..) - start"); + Log.trace("DatabaseController::writeResult(..) - start"); const resultExists = await this.getResult(record.delivId, record.repoId, record.commitSHA); if (resultExists === null) { diff --git a/packages/portal/backend/src/controllers/DeliverablesController.ts b/packages/portal/backend/src/controllers/DeliverablesController.ts index 2b52e8a71..f280cd001 100644 --- a/packages/portal/backend/src/controllers/DeliverablesController.ts +++ b/packages/portal/backend/src/controllers/DeliverablesController.ts @@ -10,14 +10,14 @@ export class DeliverablesController { private db: DatabaseController = DatabaseController.getInstance(); public async getAllDeliverables(): Promise { - Log.info("DeliverablesController::getAllGrades() - start"); + Log.trace("DeliverablesController::getAllGrades() - start"); const delivs = await this.db.getDeliverables(); return delivs; } public async getDeliverable(delivId: string): Promise { - Log.info("DeliverablesController::getDeliverable( " + delivId + " ) - start"); + Log.trace("DeliverablesController::getDeliverable( " + delivId + " ) - start"); const deliv = await this.db.getDeliverable(delivId); return deliv; @@ -48,6 +48,7 @@ export class DeliverablesController { } await this.db.writeDeliverable(deliv); // let this handle the update + Log.info("DeliverableController::saveDeliverable(..) - done"); return deliv; } @@ -87,33 +88,33 @@ export class DeliverablesController { at.closeTimestamp = deliv.closeTimestamp; const trans: DeliverableTransport = { - id: deliv.id, + id: deliv.id, URL: deliv.URL, repoPrefix: deliv.repoPrefix, teamPrefix: deliv.teamPrefix, - openTimestamp: deliv.openTimestamp, + openTimestamp: deliv.openTimestamp, closeTimestamp: deliv.closeTimestamp, - lateAutoTest: deliv.lateAutoTest, - shouldProvision: deliv.shouldProvision, - minTeamSize: deliv.teamMinSize, - maxTeamSize: deliv.teamMaxSize, - teamsSameLab: deliv.teamSameLab, + lateAutoTest: deliv.lateAutoTest, + shouldProvision: deliv.shouldProvision, + minTeamSize: deliv.teamMinSize, + maxTeamSize: deliv.teamMaxSize, + teamsSameLab: deliv.teamSameLab, studentsFormTeams: deliv.teamStudentsForm, - importURL: deliv.importURL, + importURL: deliv.importURL, - onOpenAction: '', + onOpenAction: '', onCloseAction: '', - gradesReleased: deliv.gradesReleased, + gradesReleased: deliv.gradesReleased, visibleToStudents: deliv.visibleToStudents, shouldAutoTest: deliv.shouldAutoTest, - autoTest: at, - rubric: deliv.rubric, - custom: deliv.custom + autoTest: at, + rubric: deliv.rubric, + custom: deliv.custom }; return trans; } @@ -121,20 +122,20 @@ export class DeliverablesController { public static transportToDeliverable(trans: DeliverableTransport): Deliverable { const deliv: Deliverable = { - id: trans.id, + id: trans.id, URL: trans.URL, - openTimestamp: trans.openTimestamp, + openTimestamp: trans.openTimestamp, closeTimestamp: trans.closeTimestamp, gradesReleased: trans.gradesReleased, - lateAutoTest: trans.lateAutoTest, - shouldProvision: trans.shouldProvision, - teamMinSize: trans.minTeamSize, - teamMaxSize: trans.maxTeamSize, - teamSameLab: trans.teamsSameLab, + lateAutoTest: trans.lateAutoTest, + shouldProvision: trans.shouldProvision, + teamMinSize: trans.minTeamSize, + teamMaxSize: trans.maxTeamSize, + teamSameLab: trans.teamsSameLab, teamStudentsForm: trans.studentsFormTeams, - importURL: trans.importURL, + importURL: trans.importURL, repoPrefix: trans.repoPrefix, teamPrefix: trans.teamPrefix, @@ -142,7 +143,7 @@ export class DeliverablesController { visibleToStudents: trans.visibleToStudents, shouldAutoTest: trans.shouldAutoTest, - autotest: trans.autoTest, + autotest: trans.autoTest, rubric: trans.rubric, custom: trans.custom diff --git a/packages/portal/backend/src/controllers/GradesController.ts b/packages/portal/backend/src/controllers/GradesController.ts index 6eb3c8654..18ca924be 100644 --- a/packages/portal/backend/src/controllers/GradesController.ts +++ b/packages/portal/backend/src/controllers/GradesController.ts @@ -45,16 +45,16 @@ export class GradesController { } public async getGrade(personId: string, delivId: string): Promise { - Log.info("GradesController::getGrade( " + personId + ", " + delivId + " ) - start"); + Log.trace("GradesController::getGrade( " + personId + ", " + delivId + " ) - start"); const start = Date.now(); const grade = await this.db.getGrade(personId, delivId); - Log.info("GradesController::getGrade( " + personId + ", " + delivId + " ) - done; took: " + Util.took(start)); + Log.trace("GradesController::getGrade( " + personId + ", " + delivId + " ) - done; took: " + Util.took(start)); return grade; } public async getReleasedGradesForPerson(personId: string): Promise { - Log.info("GradesController::getReleasedGradesForPerson( " + personId + " ) - start"); + Log.trace("GradesController::getReleasedGradesForPerson( " + personId + " ) - start"); const start = Date.now(); const delivc = new DeliverablesController(); @@ -91,7 +91,7 @@ export class GradesController { return g1.delivId.localeCompare(g2.delivId); }); - Log.info("GradesController::getReleasedGradesForPerson( " + personId + " ) - # grades: " + + Log.trace("GradesController::getReleasedGradesForPerson( " + personId + " ) - # grades: " + grades.length + "; took: " + Util.took(start)); return grades; } @@ -194,7 +194,7 @@ export class GradesController { */ public validateAutoTestGrade(record: AutoTestGradeTransport): string | null { // multiple returns is poor, but at least it's quick - Log.info('GradesController::validateAutoTestGrade(..) - start'); + Log.trace('GradesController::validateAutoTestGrade(..) - start'); if (typeof record === 'undefined') { const msg = 'object undefined'; @@ -253,7 +253,7 @@ export class GradesController { return msg; } - Log.info('GradesController::validateAutoTestGrade(..) - done; object is valid'); + Log.trace('GradesController::validateAutoTestGrade(..) - done; object is valid'); return null; } diff --git a/packages/portal/backend/src/controllers/RepositoryController.ts b/packages/portal/backend/src/controllers/RepositoryController.ts index 4724ed7fc..7f1f76ec7 100644 --- a/packages/portal/backend/src/controllers/RepositoryController.ts +++ b/packages/portal/backend/src/controllers/RepositoryController.ts @@ -12,34 +12,34 @@ export class RepositoryController { private db: DatabaseController = DatabaseController.getInstance(); public async getAllRepos(): Promise { - Log.info("RepositoryController::getAllRepos() - start"); + Log.trace("RepositoryController::getAllRepos() - start"); const start = Date.now(); const repos = await this.db.getRepositories(); - Log.info("RepositoryController::getAllRepos() - done; # repos: " + repos.length + "; took: " + Util.took(start)); + Log.trace("RepositoryController::getAllRepos() - done; # repos: " + repos.length + "; took: " + Util.took(start)); return repos; } public async getRepository(name: string): Promise { - Log.info("RepositoryController::getRepository( " + name + " ) - start"); + Log.trace("RepositoryController::getRepository( " + name + " ) - start"); const start = Date.now(); const repo = await this.db.getRepository(name); - Log.info("RepositoryController::getRepository( " + name + " ) - done; took: " + Util.took(start)); + Log.trace("RepositoryController::getRepository( " + name + " ) - done; took: " + Util.took(start)); return repo; } public async getReposForPerson(myPerson: Person): Promise { - Log.info("RepositoryController::getReposForPerson( " + myPerson.id + " ) - start"); + Log.trace("RepositoryController::getReposForPerson( " + myPerson.id + " ) - start"); const start = Date.now(); // TODO: this is slow; there is a faster implementation in db.getReposForPerson now, but it is untested // db.getRepositoriesForPerson(myPerson.id) const myTeams = await new TeamController().getTeamsForPerson(myPerson); - Log.info("RepositoryController::getReposForPerson( " + myPerson.id + " ) - # teams: " + myTeams.length); + Log.trace("RepositoryController::getReposForPerson( " + myPerson.id + " ) - # teams: " + myTeams.length); const myRepos: Repository[] = []; const allRepos = await this.db.getRepositories(); @@ -51,7 +51,7 @@ export class RepositoryController { } } - Log.info("RepositoryController::getReposForPerson( " + myPerson.id + + Log.trace("RepositoryController::getReposForPerson( " + myPerson.id + " ) - done; # found: " + myRepos.length + "; took: " + Util.took(start)); return myRepos; } @@ -63,11 +63,12 @@ export class RepositoryController { * @returns {Promise} */ public async updateRepository(repo: Repository): Promise { - Log.info("RepositoryController::updateRepository( .. ) - start"); if (typeof repo === 'undefined' || repo === null) { return null; } + Log.info("RepositoryController::updateRepository( " + repo.id + " ) - start"); + const existingRepo = await this.getRepository(repo.id); if (existingRepo === null) { // repo not in db, create new one @@ -87,6 +88,8 @@ export class RepositoryController { repo.custom = customCombined; await this.db.writeRepository(repo); } + + Log.info("RepositoryController::updateRepository( " + repo.id + " ) - done"); return await this.db.getRepository(repo.id); } @@ -102,15 +105,17 @@ export class RepositoryController { } const repo: Repository = { - id: name, - delivId: deliv.id, - URL: null, + id: name, + delivId: deliv.id, + URL: null, cloneURL: null, - teamIds: teamIds, - custom: custom + teamIds: teamIds, + custom: custom }; await this.db.writeRepository(repo); + + Log.info("RepositoryController::createRepository( " + name + ", .. ) - done"); return await this.db.getRepository(repo.id); } else { Log.info("RepositoryController::createRepository( " + name + ", .. ) - repository exists: " + JSON.stringify(existingRepo)); @@ -132,7 +137,7 @@ export class RepositoryController { // } public async getPeopleForRepo(repoId: string): Promise { - Log.info("RepositoryController::getPeopleForRepo( " + repoId + ", .. ) - start"); + Log.trace("RepositoryController::getPeopleForRepo( " + repoId + ", .. ) - start"); const start = Date.now(); const peopleIds: string[] = []; @@ -149,7 +154,7 @@ export class RepositoryController { } } - Log.info("RepositoryController::getPeopleForRepo( " + repoId + ", .. ) - done; # people: " + + Log.trace("RepositoryController::getPeopleForRepo( " + repoId + ", .. ) - done; # people: " + peopleIds.length + "; took: " + Util.took(start)); return peopleIds; } @@ -160,8 +165,8 @@ export class RepositoryController { } const repo: RepositoryTransport = { - id: repository.id, - URL: repository.URL, + id: repository.id, + URL: repository.URL, delivId: repository.delivId }; diff --git a/packages/portal/backend/src/controllers/TeamController.ts b/packages/portal/backend/src/controllers/TeamController.ts index 4d1b0b223..0e02ef8a5 100644 --- a/packages/portal/backend/src/controllers/TeamController.ts +++ b/packages/portal/backend/src/controllers/TeamController.ts @@ -52,12 +52,12 @@ export class TeamController { } public async getTeam(name: string): Promise { - Log.info("TeamController::getTeam( " + name + " ) - start"); + Log.trace("TeamController::getTeam( " + name + " ) - start"); const start = Date.now(); const team = await this.db.getTeam(name); - Log.info("TeamController::getTeam( " + name + " ) - done; took: " + Util.took(start)); + Log.trace("TeamController::getTeam( " + name + " ) - done; took: " + Util.took(start)); return team; } @@ -120,7 +120,7 @@ export class TeamController { } // sort by delivIds - myTeams = myTeams.sort(function(a: Team, b: Team) { + myTeams = myTeams.sort(function (a: Team, b: Team) { return a.delivId.localeCompare(b.delivId); }); @@ -213,6 +213,7 @@ export class TeamController { } const team = await this.createTeam(teamId, deliv, people, {}); + Log.info("TeamController::formTeam( " + teamId + ", ... ) - done"); return team; } @@ -252,16 +253,18 @@ export class TeamController { // } const team: Team = { - id: name, - delivId: deliv.id, - githubId: null, - URL: null, + id: name, + delivId: deliv.id, + githubId: null, + URL: null, personIds: peopleIds, - custom: custom + custom: custom // repoName: null, // repoName, // team counts above used repoName // repoUrl: null }; await this.db.writeTeam(team); + + Log.info("TeamController::teamCreate( " + name + ", ... ) - done"); return await this.db.getTeam(name); } else { // Log.info("TeamController::teamCreate( " + name + ",.. ) - team exists: " + JSON.stringify(existingTeam)); @@ -276,10 +279,10 @@ export class TeamController { public teamToTransport(team: Team): TeamTransport { const t: TeamTransport = { - id: team.id, + id: team.id, delivId: team.delivId, - people: team.personIds, - URL: team.URL + people: team.personIds, + URL: team.URL // repoName: team.repoName, // repoUrl: team.repoUrl }; diff --git a/packages/portal/backend/src/server/common/AuthRoutes.ts b/packages/portal/backend/src/server/common/AuthRoutes.ts index 50f2a45f5..fc8314dfe 100644 --- a/packages/portal/backend/src/server/common/AuthRoutes.ts +++ b/packages/portal/backend/src/server/common/AuthRoutes.ts @@ -85,10 +85,10 @@ export class AuthRoutes implements IREST { token = null; } - Log.info('AuthRoutes::getLogout(..) - user: ' + user + '; token: ' + token); + Log.trace('AuthRoutes::getLogout(..) - user: ' + user + '; token: ' + token); let payload: Payload; - const handleError = function(msg: string) { + const handleError = function (msg: string) { Log.error('AuthRoutes::getLogout(..) - ERROR: ' + msg); payload = {failure: {message: 'Logout failed: ' + msg, shouldLogout: false}}; res.send(400, payload); @@ -100,9 +100,9 @@ export class AuthRoutes implements IREST { handleError("unknown user."); } - AuthRoutes.ac.isValid(user, token).then(function(isValid) { + AuthRoutes.ac.isValid(user, token).then(function (isValid) { if (isValid === true) { - Log.info('AuthRoutes::getLogout(..) - user: ' + user + '; valid user'); + Log.trace('AuthRoutes::getLogout(..) - user: ' + user + '; valid user'); } else { // logout anyways? if your user / token is stale we still need log you out // but that could mean someone else could spoof-log you out too @@ -111,14 +111,15 @@ export class AuthRoutes implements IREST { // logout const ac = new AuthController(); return ac.removeAuthentication(user); - }).then(function(success) { + }).then(function (success) { if (success) { + Log.info('AuthRoutes::getLogout(..) - logged out; user: ' + user); payload = {success: {message: "Logout successful"}}; res.send(200, payload); } else { handleError("Logout unsuccessful."); } - }).catch(function(err) { + }).catch(function (err) { Log.error('AuthRoutes::getLogout(..) - unexpected ERROR: ' + err.message); handleError(err.message); }); @@ -128,15 +129,22 @@ export class AuthRoutes implements IREST { Log.trace('AuthRoutes::getCredentials(..) - start'); const user = req.headers.user; const token = req.headers.token; - Log.info('AuthRoutes::getCredentials(..) - user: ' + user + '; token: ' + token); + Log.trace('AuthRoutes::getCredentials(..) - user: ' + user + '; token: ' + token); let payload: AuthTransportPayload; - AuthRoutes.performGetCredentials(user, token).then(function(isPrivileged) { - payload = {success: {personId: user, token: token, isAdmin: isPrivileged.isAdmin, isStaff: isPrivileged.isStaff}}; - Log.info('AuthRoutes::getCredentials(..) - sending 200; isPriv: ' + (isPrivileged.isStaff || isPrivileged.isAdmin)); + AuthRoutes.performGetCredentials(user, token).then(function (isPrivileged) { + payload = { + success: { + personId: user, + token: token, + isAdmin: isPrivileged.isAdmin, + isStaff: isPrivileged.isStaff + } + }; + Log.trace('AuthRoutes::getCredentials(..) - sending 200; isPriv: ' + (isPrivileged.isStaff || isPrivileged.isAdmin)); res.send(200, payload); return next(true); - }).catch(function(err) { + }).catch(function (err) { Log.warn("AuthRoutes::getCredentials(..) - ERROR: " + err.message); payload = {failure: {message: err.message, shouldLogout: false}}; res.send(400, payload); @@ -144,7 +152,7 @@ export class AuthRoutes implements IREST { }); } - public static async performGetCredentials(user: string, token: string): Promise<{isAdmin: boolean, isStaff: boolean}> { + public static async performGetCredentials(user: string, token: string): Promise<{ isAdmin: boolean, isStaff: boolean }> { const isValid = await AuthRoutes.ac.isValid(user, token); Log.trace('AuthRoutes::getCredentials(..) - in isValid(..)'); if (isValid === false) { @@ -167,15 +175,15 @@ export class AuthRoutes implements IREST { /* istanbul ignore next */ public static getAuth(req: any, res: any, next: any) { - Log.info("AuthRoutes::getAuth(..) - /auth redirect start"); + Log.trace("AuthRoutes::getAuth(..) - /auth redirect start"); const config = Config.getInstance(); const setup = { - clientId: config.getProp(ConfigKey.githubClientId), - clientSecret: config.getProp(ConfigKey.githubClientSecret), - accessTokenUri: config.getProp(ConfigKey.githubHost) + '/login/oauth/access_token', + clientId: config.getProp(ConfigKey.githubClientId), + clientSecret: config.getProp(ConfigKey.githubClientSecret), + accessTokenUri: config.getProp(ConfigKey.githubHost) + '/login/oauth/access_token', authorizationUri: config.getProp(ConfigKey.githubHost) + '/login/oauth/authorize', - scopes: [''] + scopes: [''] }; const githubAuth = new ClientOAuth2(setup); @@ -200,7 +208,7 @@ export class AuthRoutes implements IREST { public static authCallback(req: any, res: any, next: any) { Log.trace("AuthRoutes::authCallback(..) - /authCallback - start"); - AuthRoutes.performAuthCallback(req.url, req.headers.host).then(function(redirectOptions) { + AuthRoutes.performAuthCallback(req.url, req.headers.host).then(function (redirectOptions) { const cookie = redirectOptions.cookie; delete redirectOptions.cookie; if (cookie !== null) { @@ -214,7 +222,7 @@ export class AuthRoutes implements IREST { res.redirect(redirectOptions, next); - }).catch(function(err) { + }).catch(function (err) { Log.error("AuthRoutes::authCallback(..) - /authCallback - ERROR: " + err); // TODO: should this be returning 400 or something? return next(false); @@ -235,11 +243,11 @@ export class AuthRoutes implements IREST { // Log.trace('req: ' + req + '; res: ' + res + '; next: ' + next); const opts = { - clientId: config.getProp(ConfigKey.githubClientId), - clientSecret: config.getProp(ConfigKey.githubClientSecret), - accessTokenUri: config.getProp(ConfigKey.githubHost) + '/login/oauth/access_token', + clientId: config.getProp(ConfigKey.githubClientId), + clientSecret: config.getProp(ConfigKey.githubClientSecret), + accessTokenUri: config.getProp(ConfigKey.githubHost) + '/login/oauth/access_token', authorizationUri: config.getProp(ConfigKey.githubHost) + '/login/oauth/authorize', - scopes: [''] + scopes: [''] }; Log.trace("AuthRoutes::performAuthCallback(..) - /authCallback - setup: " + JSON.stringify(opts)); @@ -254,10 +262,10 @@ export class AuthRoutes implements IREST { token = user.accessToken; const options: RequestInit = { - method: 'GET', + method: 'GET', headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'Portal', + 'Content-Type': 'application/json', + 'User-Agent': 'Portal', 'Authorization': 'token ' + token } // rejectUnauthorized: false, @@ -310,17 +318,17 @@ export class AuthRoutes implements IREST { Log.warn("AuthRoutes::performAuthCallback(..) - /authCallback - person (GitHub id: " + username + " ) not registered for course; redirecting to invalid user screen."); return { - cookie: null, + cookie: null, hostname: feUrl, pathname: 'invalid.html', - port: fePort + port: fePort }; } else { Log.trace("AuthRoutes::performAuthCallback(..) - /portal/authCallback - registering auth for person: " + person.githubId); const auth: Auth = { personId: person.id, // use person.id, not username (aka githubId) - token: token + token: token }; await DatabaseController.getInstance().writeAuth(auth); @@ -333,10 +341,10 @@ export class AuthRoutes implements IREST { const cookie = "token=" + token + '__' + person.id; // Firefox doesn't like multiple tokens (line above) Log.trace("AuthRoutes::performAuthCallback(..) - /authCallback - redirect homepage; cookie: " + cookie); return { - cookie: cookie, + cookie: cookie, hostname: feUrl, pathname: 'index.html', - port: fePort + port: fePort }; } } diff --git a/packages/portal/backend/src/server/common/GeneralRoutes.ts b/packages/portal/backend/src/server/common/GeneralRoutes.ts index 4e693dfa7..d7343374d 100644 --- a/packages/portal/backend/src/server/common/GeneralRoutes.ts +++ b/packages/portal/backend/src/server/common/GeneralRoutes.ts @@ -91,7 +91,7 @@ export default class GeneralRoutes implements IREST { const user = req.headers.user; const token = req.headers.token; - Log.info('GeneralRoutes::getPerson(..) - start; user: ' + user); + Log.trace('GeneralRoutes::getPerson(..) - start; user: ' + user); GeneralRoutes.performGetPerson(user, token).then(function(personTrans) { const payload: Payload = {success: personTrans}; @@ -127,7 +127,7 @@ export default class GeneralRoutes implements IREST { const user = req.headers.user; const token = req.headers.token; - Log.info('GeneralRoutes::getGrades(..) - start; user: ' + user); + Log.trace('GeneralRoutes::getGrades(..) - start; user: ' + user); GeneralRoutes.performGetGrades(user, token).then(function(grades) { const payload: GradeTransportPayload = {success: grades}; @@ -159,7 +159,7 @@ export default class GeneralRoutes implements IREST { } public static getResource(req: any, res: any, next: any) { - Log.info('GeneralRoutes::getResource(..) - start; user: ' + req.headers.user); + Log.trace('GeneralRoutes::getResource(..) - start; user: ' + req.headers.user); const auth = AdminRoutes.processAuth(req); // const user = req.headers.user; @@ -181,11 +181,11 @@ export default class GeneralRoutes implements IREST { GeneralRoutes.performGetResource(auth, path).then(function(resource: any) { const filePath = Config.getInstance().getProp(ConfigKey.persistDir) + "/runs" + path; - Log.info("GeneralRoutes::getResource(..) - start; trying to read file: " + filePath); + Log.trace("GeneralRoutes::getResource(..) - start; trying to read file: " + filePath); try { if (fs.lstatSync(filePath).isDirectory()) { - Log.info("GeneralRoutes::getResource(..) - File was actually a directory: " + filePath); + Log.trace("GeneralRoutes::getResource(..) - File was actually a directory: " + filePath); const html = GeneralRoutes.generateDirectoryHtml(filePath, path, req.url); res.writeHead(200, { 'Content-Length': Buffer.byteLength(html), @@ -206,7 +206,7 @@ export default class GeneralRoutes implements IREST { } }); rs.on("end", () => { - Log.info("GeneralRoutes::getResource(..) - done; finished reading file: " + filePath); + Log.trace("GeneralRoutes::getResource(..) - done; finished reading file: " + filePath); rs.close(); }); rs.pipe(res); @@ -238,7 +238,7 @@ export default class GeneralRoutes implements IREST { } public static async performGetResource(auth: {user: string, token: string}, path: string): Promise { - Log.info("GeneralRoutes::performGetResource( " + auth + ", " + path + " ) - start"); + Log.trace("GeneralRoutes::performGetResource( " + auth + ", " + path + " ) - start"); const host = Config.getInstance().getProp(ConfigKey.autotestUrl); const port = Config.getInstance().getProp(ConfigKey.autotestPort); @@ -300,7 +300,7 @@ export default class GeneralRoutes implements IREST { public static getRepos(req: any, res: any, next: any) { const user = req.headers.user; const token = req.headers.token; - Log.info('GeneralRoutes::getRepos(..) - start; user: ' + user); + Log.trace('GeneralRoutes::getRepos(..) - start; user: ' + user); GeneralRoutes.performGetRepos(user, token).then(function(repos) { const payload: RepositoryPayload = {success: repos}; From 080e8c0e1871fd84f5d0197355d8cd81733ef565 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 23 Sep 2022 09:31:02 -0700 Subject: [PATCH 020/104] Remove forwardCustomFields --- packages/common/types/PortalTypes.ts | 18 ----- .../src/controllers/AdminController.ts | 78 +++++++++---------- .../src/controllers/CourseController.ts | 12 --- 3 files changed, 37 insertions(+), 71 deletions(-) diff --git a/packages/common/types/PortalTypes.ts b/packages/common/types/PortalTypes.ts index 2f9f52687..15d8b2686 100644 --- a/packages/common/types/PortalTypes.ts +++ b/packages/common/types/PortalTypes.ts @@ -336,21 +336,3 @@ export interface ClasslistTransport { SEC: string; LAB: string; } - -// This list is not exhaustive at all -export enum TransportKind { - CLASSLIST_CHANGES, - CONFIG, - COURSE, - PROVISION, - AUTH, - STUDENT, - DELIVERABLE, - TEAM, - TEAM_FORMATION, - GRADE, - GRADE_REPORT, // TODO Should this be here? GradeReport is a ContainerType - AUTOTEST_DASHBOARD, - AUTOTEST_RESULT_SUMMARY, - CLASSLIST, -} diff --git a/packages/portal/backend/src/controllers/AdminController.ts b/packages/portal/backend/src/controllers/AdminController.ts index 5674d96a0..378848c65 100644 --- a/packages/portal/backend/src/controllers/AdminController.ts +++ b/packages/portal/backend/src/controllers/AdminController.ts @@ -12,7 +12,7 @@ import { ProvisionTransport, RepositoryTransport, StudentTransport, - TeamTransport, TransportKind + TeamTransport, } from '../../../../common/types/PortalTypes'; import Util from "../../../../common/Util"; import {Factory} from "../Factory"; @@ -270,44 +270,7 @@ export class AdminController { for (const result of allResults) { const repoId = result.input.target.repoId; if (results.length < NUM_RESULTS) { - const resultSummary = await this.clipAutoTestResult(result); - - let testPass: string[] = []; - let testFail: string[] = []; - let testSkip: string[] = []; - let testError: string[] = []; - - const cc = await Factory.getCourseController(this.gh); - let custom = cc.forwardCustomFields(resultSummary, TransportKind.AUTOTEST_DASHBOARD); - - if (typeof result.output !== 'undefined' && typeof result.output.report !== 'undefined') { - const report: GradeReport = result.output.report; - if (typeof report.passNames !== 'undefined') { - testPass = report.passNames; - } - if (typeof report.failNames !== 'undefined') { - testFail = report.failNames; - } - if (typeof report.skipNames !== 'undefined') { - testSkip = report.skipNames; - } - if (typeof report.errorNames !== 'undefined') { - testError = report.errorNames; - } - if (typeof report.custom !== 'undefined') { - custom = {...custom, ...cc.forwardCustomFields(report, TransportKind.GRADE_REPORT)}; - } - } - - const resultTrans: AutoTestDashboardTransport = { - ...resultSummary, - testPass: testPass, - testFail: testFail, - testError: testError, - testSkip: testSkip, - custom: custom, - }; - + const resultTrans = await this.createDashboardTransport(result); // just return the first result for a repo, unless they are specified if (reqRepoId !== 'any' || repoIds.indexOf(repoId) < 0) { results.push(resultTrans); @@ -321,6 +284,40 @@ export class AdminController { return results; } + private async createDashboardTransport(result: Result): Promise { + const resultSummary = await this.clipAutoTestResult(result); + + let testPass: string[] = []; + let testFail: string[] = []; + let testSkip: string[] = []; + let testError: string[] = []; + + if (typeof result.output !== 'undefined' && typeof result.output.report !== 'undefined') { + const report: GradeReport = result.output.report; + if (typeof report.passNames !== 'undefined') { + testPass = report.passNames; + } + if (typeof report.failNames !== 'undefined') { + testFail = report.failNames; + } + if (typeof report.skipNames !== 'undefined') { + testSkip = report.skipNames; + } + if (typeof report.errorNames !== 'undefined') { + testError = report.errorNames; + } + } + + return { + ...resultSummary, + testPass: testPass, + testFail: testFail, + testError: testError, + testSkip: testSkip, + custom: {}, + }; + } + public async matchResults(reqDelivId: string, reqRepoId: string, kind: ResultsKind): Promise { Log.trace("AdminController::matchResults(..) - start"); const start = Date.now(); @@ -445,7 +442,6 @@ export class AdminController { } const state = this.selectState(result); - const custom = cc.forwardCustomFields(result, TransportKind.AUTOTEST_RESULT_SUMMARY); return { repoId: repoId, @@ -458,7 +454,7 @@ export class AdminController { scoreOverall: scoreOverall, scoreCover: scoreCover, scoreTests: scoreTest, - custom: custom + custom: {}, }; } diff --git a/packages/portal/backend/src/controllers/CourseController.ts b/packages/portal/backend/src/controllers/CourseController.ts index 4ea317e42..c150eb219 100644 --- a/packages/portal/backend/src/controllers/CourseController.ts +++ b/packages/portal/backend/src/controllers/CourseController.ts @@ -2,7 +2,6 @@ import Log from "../../../../common/Log"; import {Deliverable, Grade, Person, Repository, Team} from "../Types"; import {CommitTarget} from "../../../../common/types/ContainerTypes"; -import {TransportKind} from "../../../../common/types/PortalTypes"; import {DatabaseController} from "./DatabaseController"; import {IGitHubController} from "./GitHubController"; import {GradesController} from "./GradesController"; @@ -68,13 +67,6 @@ export interface ICourseController { */ finalizeProvisionedRepo(repo: Repository, teams: Team[]): Promise; - /** - * For forwarding custom fields from a record to its respective transport - * @param record - * @param kind - */ - forwardCustomFields(record: any, kind: TransportKind): any; - /** * For forcing certain push events to the express queue * e.g.: Commits on master getting automatically graded @@ -211,10 +203,6 @@ export class CourseController implements ICourseController { return true; } - public forwardCustomFields(record: any, kind: TransportKind): any { - return {}; - } - public async shouldPrioritizePushEvent(info: CommitTarget): Promise { Log.warn(`CourseController::shouldPrioritizePushEvent(${info.commitSHA}) - Default impl; returning false`); return false; From 81b079f34036a8ceb641ca638fcd0ad111fcd8d1 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 23 Sep 2022 10:14:36 -0700 Subject: [PATCH 021/104] Record the person who pushed the commit --- packages/autotest/src/github/GitHubUtil.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index 1fa2043f0..3f1ed256a 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -187,6 +187,7 @@ export class GitHubUtil { const projectURL = payload.repository.html_url; const cloneURL = payload.repository.clone_url; const ref = payload.ref; + const pusher = await new ClassPortal().getPersonId(payload.pusher.name); Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; projectURL: " + projectURL + "; ref: " + ref); if (payload.deleted === true && payload.head_commit === null) { @@ -227,7 +228,7 @@ export class GitHubUtil { delivId: backendConfig.defaultDeliverable, repoId: repo, botMentioned: false, // not explicitly invoked - personId: null, // not explicitly requested + personId: pusher?.personId ?? null, kind: 'push', cloneURL, commitSHA, From 08e12416acda57c0d79e2b90436fe4b5bd6b3c1e Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 10:50:29 -0700 Subject: [PATCH 022/104] use pusher instead of repo to determine queue over-use --- packages/autotest/src/autotest/AutoTest.ts | 8 +-- packages/autotest/src/autotest/Queue.ts | 69 +++++++++++----------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/autotest/src/autotest/AutoTest.ts b/packages/autotest/src/autotest/AutoTest.ts index 2b3be89ba..1c45bfe5b 100644 --- a/packages/autotest/src/autotest/AutoTest.ts +++ b/packages/autotest/src/autotest/AutoTest.ts @@ -144,8 +144,8 @@ export abstract class AutoTest implements IAutoTest { return; } - if (this.expressQueue.hasWaitingJobForRequester(input) === false) { - // add to express queue + if (this.expressQueue.numberJobsForPerson(input) < 1) { + // add to express queue since they are not already on it this.expressQueue.push(input); // if job is on any other queue, remove it @@ -176,8 +176,8 @@ export abstract class AutoTest implements IAutoTest { // only add job if it is not already on express if (this.expressQueue.indexOf(input) < 0) { - const standardJobCount = this.standardQueue.numberJobsForRepo(input); - const regressionJobCount = this.regressionQueue.numberJobsForRepo(input); + const standardJobCount = this.standardQueue.numberJobsForPerson(input); + const regressionJobCount = this.regressionQueue.numberJobsForPerson(input); if (standardJobCount < this.MAX_STANDARD_JOBS) { this.standardQueue.push(input); diff --git a/packages/autotest/src/autotest/Queue.ts b/packages/autotest/src/autotest/Queue.ts index e24519863..0a19e7475 100644 --- a/packages/autotest/src/autotest/Queue.ts +++ b/packages/autotest/src/autotest/Queue.ts @@ -136,54 +136,55 @@ export class Queue { return this.data.length > 0; } - /** - * Returns true if a job is already waiting for a requester on this queue. - * - * @param input - */ - public hasWaitingJobForRequester(input: ContainerInput): boolean { - for (const job of this.data) { - if (input.target.personId !== null && typeof input.target.personId !== "undefined" && - job.target.personId === input.target.personId) { - return true; - } - } - for (const job of this.slots) { - if (input.target.personId !== null && typeof input.target.personId !== "undefined" && - job.target.personId === input.target.personId) { - return true; - } - } - return false; - } + // /** + // * Returns true if a job is already waiting for a requester on this queue. + // * + // * @param input + // */ + // public hasWaitingJobForRequester(input: ContainerInput): boolean { + // for (const job of this.data) { + // if (input.target.personId !== null && typeof input.target.personId !== "undefined" && + // job.target.personId === input.target.personId) { + // return true; + // } + // } + // for (const job of this.slots) { + // if (input.target.personId !== null && typeof input.target.personId !== "undefined" && + // job.target.personId === input.target.personId) { + // return true; + // } + // } + // return false; + // } /** - * Returns the number of queued or executing jobs for a repo. + * Returns the number of queued jobs for a person. * - * NOTE: it would be better for this to be per requester, but - * often push events do not have this info. + * Does _NOT_ include executing jobs, as these could be placed + * there by the scheduler, not by the requester (e.g., a standard + * job could be placed on the express queue because there is a free + * slot, this placement should not stop the requester from having + * a future request be put on the express queue while the non-requested + * one is evaluated). * * @param input */ - public numberJobsForRepo(input: ContainerInput): number { + public numberJobsForPerson(input: ContainerInput): number { let count = 0; for (const job of this.data) { - if (input.target.repoId !== null && typeof input.target.repoId !== "undefined" && - job.target.repoId === input.target.repoId) { - if (typeof input.target.adminRequest !== "undefined" && input.target.adminRequest !== null && - input.target.adminRequest === true) { + if (job.target?.personId === input.target?.personId) { + if (input.target?.adminRequest === true) { // admin requests shouldn't count towards repo totals } else { count++; } } } - for (const job of this.slots) { - if (input.target.repoId !== null && typeof input.target.repoId !== "undefined" && - job.target.repoId === input.target.repoId) { - count++; - } - } + // for (const job of this.slots) { + // if (job.target?.personId === input.target?.personId) { + // count++; + // } + // } return count; } From c26bdc388ed2e1bc41293cf2751ea845242962cd Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 11:07:44 -0700 Subject: [PATCH 023/104] backfill standard jobs to express slots, if there is idle capacity --- packages/autotest/src/autotest/AutoTest.ts | 18 +++++++++++++- packages/autotest/src/autotest/Queue.ts | 25 +------------------- packages/autotest/test/GitHubAutoTestSpec.ts | 10 +++++--- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/autotest/src/autotest/AutoTest.ts b/packages/autotest/src/autotest/AutoTest.ts index 1c45bfe5b..be4a33459 100644 --- a/packages/autotest/src/autotest/AutoTest.ts +++ b/packages/autotest/src/autotest/AutoTest.ts @@ -101,7 +101,7 @@ export abstract class AutoTest implements IAutoTest { * * @private */ - private readonly MAX_STANDARD_JOBS: number = 4; + private readonly MAX_STANDARD_JOBS: number = 3; /** * The maximum number of jobs a single user can have on the regression queue @@ -178,6 +178,8 @@ export abstract class AutoTest implements IAutoTest { const standardJobCount = this.standardQueue.numberJobsForPerson(input); const regressionJobCount = this.regressionQueue.numberJobsForPerson(input); + + // this is fairly permissive; only queued jobs (not executing jobs) are counted if (standardJobCount < this.MAX_STANDARD_JOBS) { this.standardQueue.push(input); @@ -188,6 +190,10 @@ export abstract class AutoTest implements IAutoTest { input.target.repoId + "; has #" + standardJobCount + " standard jobs queued and #" + regressionJobCount + " regression jobs queued"); + + // NOTE: this _could_ post a warning back to the user + // that their priority is lowered due to excess jobs + this.addToRegressionQueue(input); } @@ -302,6 +308,10 @@ export abstract class AutoTest implements IAutoTest { Log.trace("AutoTest::tick::switchQueues(..) - switched: " + input.target.commitSHA); }; + // + // handle the queues in order: express -> standard -> regression + // + // fill all express execution slots with express jobs while (this.expressQueue.hasCapacity() && this.expressQueue.hasWaitingJobs()) { tickQueue(this.expressQueue); @@ -331,6 +341,12 @@ export abstract class AutoTest implements IAutoTest { tickQueue(this.regressionQueue); } + // backfill standard jobs to the express queue, if there is pace + while (this.expressQueue.hasCapacity() && this.standardQueue.hasWaitingJobs()) { + switchQueues(this.standardQueue.peek(), this.standardQueue, this.expressQueue, true); + tickQueue(this.expressQueue); + } + // finally, run the regression queue with any of its jobs that are waiting while (this.regressionQueue.hasCapacity() && this.regressionQueue.hasWaitingJobs()) { tickQueue(this.regressionQueue); diff --git a/packages/autotest/src/autotest/Queue.ts b/packages/autotest/src/autotest/Queue.ts index 0a19e7475..fe5cf7349 100644 --- a/packages/autotest/src/autotest/Queue.ts +++ b/packages/autotest/src/autotest/Queue.ts @@ -39,9 +39,7 @@ export class Queue { */ public push(input: ContainerInput): number { - if (typeof input.target.adminRequest !== "undefined" && - input.target.adminRequest !== null && - input.target.adminRequest === true) { + if (input?.target?.adminRequest === true) { // put admin requests on the front of the queue Log.info("Queue:push(..) - admin request; pushing to head of queue"); this.pushFirst(input); @@ -136,27 +134,6 @@ export class Queue { return this.data.length > 0; } - // /** - // * Returns true if a job is already waiting for a requester on this queue. - // * - // * @param input - // */ - // public hasWaitingJobForRequester(input: ContainerInput): boolean { - // for (const job of this.data) { - // if (input.target.personId !== null && typeof input.target.personId !== "undefined" && - // job.target.personId === input.target.personId) { - // return true; - // } - // } - // for (const job of this.slots) { - // if (input.target.personId !== null && typeof input.target.personId !== "undefined" && - // job.target.personId === input.target.personId) { - // return true; - // } - // } - // return false; - // } - /** * Returns the number of queued jobs for a person. * diff --git a/packages/autotest/test/GitHubAutoTestSpec.ts b/packages/autotest/test/GitHubAutoTestSpec.ts index 7c8f32b10..12802abbb 100644 --- a/packages/autotest/test/GitHubAutoTestSpec.ts +++ b/packages/autotest/test/GitHubAutoTestSpec.ts @@ -147,6 +147,9 @@ describe("GitHubAutoTest", () => { await at.handlePushEvent(pushes[3]); await at.handlePushEvent(pushes[4]); await at.handlePushEvent(pushes[5]); + await at.handlePushEvent(pushes[6]); + await at.handlePushEvent(pushes[7]); + await at.handlePushEvent(pushes[8]); Log.test("all pushes sent"); // to see at what admin pushes look like @@ -161,16 +164,17 @@ describe("GitHubAutoTest", () => { await Util.delay(10); // all pushes should be here - expect(allData.pushes.length).to.equal(6); + expect(allData.pushes.length).to.equal(9); const eq = (at["expressQueue"] as any); const sq = (at["standardQueue"] as any); const rq = (at["regressionQueue"] as any); Log.test("about to check values"); - expect(eq.slots).to.have.length(0); // nothing should be running on express + expect(eq.slots).to.have.length(2); // two jobs should have been backfilled to express expect(eq.data).to.have.length(0); // nothing should be queued on express expect(sq.slots).to.have.length(2); // two should be running on standard - expect(sq.data).to.have.length(2); // two should be waiting on standard + expect(sq.data).to.have.length(3); // one should be waiting on standard expect(rq.slots).to.have.length(1); // one should be running on regression + // this is the main check: if this all worked, a job should have been pushed onto the regression queue expect(rq.data).to.have.length(1); // one should be queued on regression Log.test("values checked"); }).timeout(WAIT * 3); From 4f4bdd732037eb71fd86054367f53b776f955246 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 15:55:29 -0700 Subject: [PATCH 024/104] decrease logging --- packages/autotest/src/github/GitHubUtil.ts | 8 ++++---- .../backend/src/controllers/DatabaseController.ts | 2 +- .../backend/src/server/common/AutoTestRoutes.ts | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index 5b5d85b15..4704c922e 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -103,13 +103,13 @@ export class GitHubUtil { const requestor = String(payload.comment.user.login); // .toLowerCase(); const message = payload.comment.body; - Log.info("GitHubUtil::processComment(..) - 1"); + Log.trace("GitHubUtil::processComment(..) - 1"); const cp = new ClassPortal(); const config = await cp.getConfiguration(); const delivId = GitHubUtil.parseDeliverableFromComment(message, config.deliverableIds); - Log.info("GitHubUtil::processComment(..) - 2"); + Log.trace("GitHubUtil::processComment(..) - 2"); const flags: string[] = GitHubUtil.parseCommandsFromComment(message); @@ -118,7 +118,7 @@ export class GitHubUtil { const repoId = payload.repository.name; - Log.info("GitHubUtil::processComment(..) - 3"); + Log.trace("GitHubUtil::processComment(..) - 3"); // const timestamp = new Date(payload.comment.updated_at).getTime(); // updated so they can't add requests to a past comment const timestamp = Date.now(); // set timestamp to the time the commit was made @@ -138,7 +138,7 @@ export class GitHubUtil { kind = 'check'; } - Log.info("GitHubUtil::processComment(..) - 4"); + Log.trace("GitHubUtil::processComment(..) - 4"); const commentEvent: CommitTarget = { delivId, diff --git a/packages/portal/backend/src/controllers/DatabaseController.ts b/packages/portal/backend/src/controllers/DatabaseController.ts index 2e9d5f3bd..f990fd2f3 100644 --- a/packages/portal/backend/src/controllers/DatabaseController.ts +++ b/packages/portal/backend/src/controllers/DatabaseController.ts @@ -420,7 +420,7 @@ export class DatabaseController { public async writeAudit(label: AuditLabel, personId: string, before: any, after: any, custom: any): Promise { try { // Log.info("DatabaseController::writeAudit(..) - start"); - Log.info("DatabaseController::writeAudit( " + label + ", " + personId + ", hasBefore: " + + Log.trace("DatabaseController::writeAudit( " + label + ", " + personId + ", hasBefore: " + !Util.isEmpty(before) + ", hasAfter: " + !Util.isEmpty(after) + " ) - start"); let finalLabel = label + '_'; diff --git a/packages/portal/backend/src/server/common/AutoTestRoutes.ts b/packages/portal/backend/src/server/common/AutoTestRoutes.ts index c91e27ae7..6b0678560 100644 --- a/packages/portal/backend/src/server/common/AutoTestRoutes.ts +++ b/packages/portal/backend/src/server/common/AutoTestRoutes.ts @@ -104,7 +104,7 @@ export class AutoTestRoutes implements IREST { } public static atConfiguration(req: any, res: any, next: any) { - Log.info('AutoTestRouteHandler::atConfiguration(..) - /at - start'); + Log.trace('AutoTestRouteHandler::atConfiguration(..) - /at - start'); const start = Date.now(); let payload: ClassyConfigurationPayload; @@ -114,15 +114,15 @@ export class AutoTestRoutes implements IREST { } else { const name = Config.getInstance().getProp(ConfigKey.name); - Log.info('AutoTestRouteHandler::atConfiguration(..) - name: ' + name + '; took: ' + Util.took(start)); + Log.trace('AutoTestRouteHandler::atConfiguration(..) - name: ' + name + '; took: ' + Util.took(start)); const cc = new AdminController(new GitHubController(GitHubActions.getInstance())); let defaultDeliverable: string | null = null; - Log.info('AutoTestRouteHandler::atConfiguration(..) - cc; took: ' + Util.took(start)); + Log.trace('AutoTestRouteHandler::atConfiguration(..) - cc; took: ' + Util.took(start)); cc.getCourse().then(function(course) { defaultDeliverable = course.defaultDeliverableId; - Log.info('AutoTestRouteHandler::atConfiguration(..) - default: ' + defaultDeliverable + '; took: ' + Util.took(start)); + Log.trace('AutoTestRouteHandler::atConfiguration(..) - default: ' + defaultDeliverable + '; took: ' + Util.took(start)); return cc.getDeliverables(); }).then(function(deliverables) { const delivIds = []; @@ -131,7 +131,7 @@ export class AutoTestRoutes implements IREST { } payload = {success: {defaultDeliverable: defaultDeliverable, deliverableIds: delivIds}}; - Log.info('AutoTestRouteHandler::atConfiguration(..) - /at - done; took: ' + Util.took(start)); + Log.trace('AutoTestRouteHandler::atConfiguration(..) - /at - done; took: ' + Util.took(start)); res.send(200, payload); return next(true); }).catch(function(err) { @@ -301,7 +301,7 @@ export class AutoTestRoutes implements IREST { } public static atPersonId(req: any, res: any, next: any) { - Log.info('AutoTestRouteHandler::atPersonId(..) - /isStaff/:githubId - start GET'); + Log.trace('AutoTestRouteHandler::atPersonId(..) - /isStaff/:githubId - start GET'); const start = Date.now(); let payload: Payload; @@ -330,7 +330,7 @@ export class AutoTestRoutes implements IREST { } public static atGetResult(req: any, res: any, next: any) { - Log.info('AutoTestRouteHandler::atGetResult(..) - /at/result/:delivId/:repoId - start GET'); + Log.trace('AutoTestRouteHandler::atGetResult(..) - /at/result/:delivId/:repoId - start GET'); let payload: AutoTestResultPayload; From cab07164af28871f6b5bdfd64f5f34ffab930d28 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 23 Sep 2022 17:15:22 -0700 Subject: [PATCH 025/104] Display labs on the Teams page --- .../frontend/src/app/views/AdminTeamsTab.ts | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts index 59fb7ec79..3eb259903 100644 --- a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts +++ b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts @@ -90,6 +90,14 @@ export class AdminTeamsTab extends AdminPage { sortDown: false, style: 'padding-left: 1em; padding-right: 1em;' }, + { + id: 'labs', + text: 'Labs', + sortable: true, + defaultSort: false, + sortDown: false, + style: 'padding-left: 1em; padding-right: 1em;' + }, { id: 'p1', text: 'First Member', @@ -158,10 +166,13 @@ export class AdminTeamsTab extends AdminPage { teamDisplay = team.id; } + const labs = this.getLabsCell(team.people); + const row: TableCell[] = [ {value: count, html: count + ''}, {value: team.id, html: teamDisplay}, {value: repoName, html: repoDisplay}, + {value: labs, html: labs}, {value: p1, html: p1}, {value: p2, html: p2}, {value: p3, html: p3} @@ -204,6 +215,17 @@ export class AdminTeamsTab extends AdminPage { } } + private getLabsCell(people: string[]): string { + const labs = people + .map((personId) => this.getPerson(personId)?.labId) + .filter((lab) => !!lab); + return [...new Set(labs)].sort().join(','); + } + + private getPerson(personId: string): StudentTransport | null { + return this.students.find((student) => student.id === personId) ?? null; + } + /** * Convert personId to a more useful representation for staff to understand. * @@ -214,31 +236,26 @@ export class AdminTeamsTab extends AdminPage { */ private getPersonCell(personId: string): string { let render = personId; + const student = this.getPerson(personId); - try { - for (const student of this.students) { - if (student.id === personId) { - - if (student.githubId === student.id) { - // render staff (whose GitHub ids should match their CSIDs (according to the system) - if (student.userUrl !== null && student.userUrl.startsWith('http') === true) { - render = 'Staff: ' + student.githubId + ''; - } else { - render = 'Staff: ' + student.githubId; - } - } else { - // render students - if (student.userUrl !== null && student.userUrl.startsWith('http') === true) { - render = '' + - student.githubId + ' (' + student.id + ')'; - } else { - render = student.githubId + " (" + student.id + ")"; - } - } + if (student?.id === personId) { + + if (student.githubId === student.id) { + // render staff (whose GitHub ids should match their CSIDs (according to the system) + if (student.userUrl !== null && student.userUrl.startsWith('http') === true) { + render = 'Staff: ' + student.githubId + ''; + } else { + render = 'Staff: ' + student.githubId; + } + } else { + // render students + if (student.userUrl !== null && student.userUrl.startsWith('http') === true) { + render = '' + + student.githubId + ' (' + student.id + ')'; + } else { + render = student.githubId + " (" + student.id + ")"; } } - } catch (err) { - Log.error("AdminTeamsTab::getPersonCell( " + personId + " ) - ERROR: " + err.message); } return render; } From 53bce2e306baf117696aa1f76abf44828ae1c047 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 19:42:21 -0700 Subject: [PATCH 026/104] reduce verbosity --- packages/autotest/src/autotest/AutoTest.ts | 2 +- packages/autotest/src/github/GitHubAutoTest.ts | 2 +- packages/autotest/src/server/RouteHandler.ts | 2 +- packages/portal/backend/src/Factory.ts | 2 +- .../backend/src/controllers/GradesController.ts | 2 +- .../backend/src/controllers/ResultsController.ts | 9 +++++---- .../backend/src/server/common/AutoTestRoutes.ts | 16 ++++++++-------- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/autotest/src/autotest/AutoTest.ts b/packages/autotest/src/autotest/AutoTest.ts index be4a33459..e433bbee5 100644 --- a/packages/autotest/src/autotest/AutoTest.ts +++ b/packages/autotest/src/autotest/AutoTest.ts @@ -232,7 +232,7 @@ export abstract class AutoTest implements IAutoTest { */ public tick(): void { try { - Log.info("AutoTest::tick(..) - start; " + + Log.trace("AutoTest::tick(..) - start; " + "express - #wait: " + this.expressQueue.length() + ", #run: " + this.expressQueue.numRunning() + "; " + "standard - #wait: " + this.standardQueue.length() + ", #run: " + this.standardQueue.numRunning() + "; " + "regression - #wait: " + this.regressionQueue.length() + ", #run: " + this.regressionQueue.numRunning() + "."); diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index cf2ab94bf..dfc71eebe 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -579,7 +579,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { const feedbackDelay: string | null = await this.requestFeedbackDelay(delivId, personId, data.input.target.timestamp); const futureTarget: boolean = standardFeedbackRequested !== null && (standardFeedbackRequested.timestamp > Date.now()); - Log.info(`GitHubAutoTest::processExecution() - Target is from the future: ${futureTarget}`); + Log.trace(`GitHubAutoTest::processExecution() - Target is from the future: ${futureTarget}`); if (data.output.postbackOnComplete === true) { // handle 'free' feedback as specified by the grading container diff --git a/packages/autotest/src/server/RouteHandler.ts b/packages/autotest/src/server/RouteHandler.ts index 6cd865477..4fa8be928 100644 --- a/packages/autotest/src/server/RouteHandler.ts +++ b/packages/autotest/src/server/RouteHandler.ts @@ -89,7 +89,7 @@ export default class RouteHandler { secretVerified = (githubSecret === computed); if (secretVerified === true) { - Log.info("RouteHandler::postGithubHook(..) - webhook secret verified: " + secretVerified + + Log.trace("RouteHandler::postGithubHook(..) - webhook secret verified: " + secretVerified + "; took: " + Util.took(start)); } else { Log.warn("RouteHandler::postGithubHook(..) - webhook secrets do not match"); diff --git a/packages/portal/backend/src/Factory.ts b/packages/portal/backend/src/Factory.ts index c74d1dfec..345c064bd 100644 --- a/packages/portal/backend/src/Factory.ts +++ b/packages/portal/backend/src/Factory.ts @@ -105,7 +105,7 @@ export class Factory { // if this fails an error will be raised and the default view will be provided in the catch below const constructorName = Object.keys(plug)[0]; const handler = new plug[constructorName](ghController); - Log.info("Factory::getCourseController() - handler instantiated"); + Log.trace("Factory::getCourseController() - handler instantiated"); return handler; } catch (err) { const msg = "Factory::getCourseController() - src/custom/CustomCourseController.ts must be defined"; diff --git a/packages/portal/backend/src/controllers/GradesController.ts b/packages/portal/backend/src/controllers/GradesController.ts index 18ca924be..1c7642aee 100644 --- a/packages/portal/backend/src/controllers/GradesController.ts +++ b/packages/portal/backend/src/controllers/GradesController.ts @@ -168,7 +168,7 @@ export class GradesController { } public async saveGrade(grade: Grade): Promise { - Log.info("GradesController::saveGrade( .. ) - start; person: " + + Log.trace("GradesController::saveGrade( .. ) - start; person: " + grade.personId + "; deliv: " + grade.delivId + "; score: " + grade.score); const start = Date.now(); diff --git a/packages/portal/backend/src/controllers/ResultsController.ts b/packages/portal/backend/src/controllers/ResultsController.ts index 801662742..9f41c420b 100644 --- a/packages/portal/backend/src/controllers/ResultsController.ts +++ b/packages/portal/backend/src/controllers/ResultsController.ts @@ -229,7 +229,7 @@ export class ResultsController { return reportMsg; } - Log.info('ResultsController::validateAutoTestResult(..) - done; object is valid'); + Log.trace('ResultsController::validateAutoTestResult(..) - done; object is valid'); return null; } @@ -313,12 +313,13 @@ export class ResultsController { Log.error('ResultsController::validateGradeReport(..) - ERROR: ' + msg); return msg; } - Log.info('ResultsController::validateGradeReport(..) - done; report is valid'); + + Log.trace('ResultsController::validateGradeReport(..) - done; report is valid'); return null; // everything is good } public async getResultsForDeliverable(delivId: string, kind: ResultsKind = ResultsKind.ALL) { - Log.info("ResultsController::getResultsForDeliverable( " + delivId + " ) - start"); + Log.trace("ResultsController::getResultsForDeliverable( " + delivId + " ) - start"); const start = Date.now(); let outcome: Result[] = []; @@ -338,7 +339,7 @@ export class ResultsController { } public async getResultsForRepo(repoId: string) { - Log.info("ResultsController::getResultsForRepo( " + repoId + " ) - start"); + Log.trace("ResultsController::getResultsForRepo( " + repoId + " ) - start"); const start = Date.now(); const outcome = await DatabaseController.getInstance().getResultsForRepo(repoId); diff --git a/packages/portal/backend/src/server/common/AutoTestRoutes.ts b/packages/portal/backend/src/server/common/AutoTestRoutes.ts index 6b0678560..141aa8182 100644 --- a/packages/portal/backend/src/server/common/AutoTestRoutes.ts +++ b/packages/portal/backend/src/server/common/AutoTestRoutes.ts @@ -63,7 +63,7 @@ export class AutoTestRoutes implements IREST { } public static atContainerDetails(req: any, res: any, next: any) { - Log.info('AutoTestRouteHandler::atContainerDetails(..) - /at/container/:delivId - start GET'); + Log.trace('AutoTestRouteHandler::atContainerDetails(..) - /at/container/:delivId - start GET'); const start = Date.now(); let payload: AutoTestConfigPayload; @@ -74,7 +74,7 @@ export class AutoTestRoutes implements IREST { const delivId = req.params.delivId; const name = Config.getInstance().getProp(ConfigKey.name); - Log.info('AutoTestRouteHandler::atContainerDetails(..) - name: ' + name + '; delivId: ' + delivId); + Log.trace('AutoTestRouteHandler::atContainerDetails(..) - name: ' + name + '; delivId: ' + delivId); const dc = new DeliverablesController(); dc.getDeliverable(delivId).then(function(deliv) { @@ -90,7 +90,7 @@ export class AutoTestRoutes implements IREST { lateAutoTest: deliv.lateAutoTest }; payload = {success: at}; - Log.info('AutoTestRouteHandler::atContainerDetails(..) - /at/container/:delivId - done; took: ' + Util.took(start)); + Log.trace('AutoTestRouteHandler::atContainerDetails(..) - /at/container/:delivId - done; took: ' + Util.took(start)); res.send(200, payload); return next(true); } else { @@ -164,7 +164,7 @@ export class AutoTestRoutes implements IREST { // } public static atGrade(req: any, res: any, next: any) { - Log.info('AutoTestRouteHandler::atGrade(..) - start'); + Log.trace('AutoTestRouteHandler::atGrade(..) - start'); const start = Date.now(); let payload: Payload; @@ -177,7 +177,7 @@ export class AutoTestRoutes implements IREST { AutoTestRoutes.performPostGrade(gradeRecord).then(function(saved: any) { payload = {success: {success: saved}}; - Log.info('AutoTestRouteHandler::atGrade(..) - done; took: ' + Util.took(start)); + Log.trace('AutoTestRouteHandler::atGrade(..) - done; took: ' + Util.took(start)); res.send(200, payload); return next(true); }).catch(function(err) { @@ -212,7 +212,7 @@ export class AutoTestRoutes implements IREST { * @param next */ public static atPostResult(req: any, res: any, next: any) { - Log.info('AutoTestRouteHandler::atPostResult(..) - start'); + Log.trace('AutoTestRouteHandler::atPostResult(..) - start'); const start = Date.now(); let payload: Payload = null; @@ -225,7 +225,7 @@ export class AutoTestRoutes implements IREST { // Log.trace('AutoTestRouteHandler::atPostResult(..) - body: ' + JSON.stringify(resultRecord)); AutoTestRoutes.performPostResult(resultRecord).then(function() { payload = {success: {message: 'Result received'}}; - Log.info('AutoTestRouteHandler::atPostResult(..) - done; took: ' + Util.took(start)); + Log.trace('AutoTestRouteHandler::atPostResult(..) - done; took: ' + Util.took(start)); res.send(200, payload); return next(true); }).catch(function(err) { @@ -267,7 +267,7 @@ export class AutoTestRoutes implements IREST { * @param next */ public static async atIsStaff(req: any, res: any, next: any) { - Log.info('AutoTestRouteHandler::atIsStaff(..) - /isStaff/:githubId - start'); + Log.trace('AutoTestRouteHandler::atIsStaff(..) - /isStaff/:githubId - start'); const start = Date.now(); let payload: AutoTestAuthPayload; From 0f8c0cd51d942664f1d4fbe674e042c4eebbb6d4 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 19:52:53 -0700 Subject: [PATCH 027/104] fix missing icon, improve padded score sorting --- .../portal/frontend/src/app/views/AdminGradesTab.ts | 7 ++++--- .../portal/frontend/src/app/views/AdminResultsTab.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/portal/frontend/src/app/views/AdminGradesTab.ts b/packages/portal/frontend/src/app/views/AdminGradesTab.ts index 2c6d2aa08..e3957193e 100644 --- a/packages/portal/frontend/src/app/views/AdminGradesTab.ts +++ b/packages/portal/frontend/src/app/views/AdminGradesTab.ts @@ -130,6 +130,7 @@ export class AdminGradesTab extends AdminPage { const hoverComment = AdminGradesTab.makeHTMLSafe(grade.comment); // let score = ''; let score: number | string = ''; + let scorePrepend = ''; if (grade.score !== null && grade.score >= 0) { score = grade.score; if (score === 100) { @@ -138,17 +139,17 @@ export class AdminGradesTab extends AdminPage { // two decimal places score = score.toFixed(2); // prepend space (not 100) - score = " " + score; + scorePrepend = " " + scorePrepend; if (grade.score < 10) { // prepend with extra space if < 10 - score = " " + score; + scorePrepend = " " + scorePrepend; } } // score = grade.score + ''; } let html; if (score !== '' && grade.URL !== null) { - html = `${score}`; + html = scorePrepend + `${score}`; } else if (score !== '' && grade.URL === null) { html = `
${score}
`; } else { diff --git a/packages/portal/frontend/src/app/views/AdminResultsTab.ts b/packages/portal/frontend/src/app/views/AdminResultsTab.ts index 64bc7d594..1cd516d2f 100644 --- a/packages/portal/frontend/src/app/views/AdminResultsTab.ts +++ b/packages/portal/frontend/src/app/views/AdminResultsTab.ts @@ -180,6 +180,7 @@ export class AdminResultsTab extends AdminPage { const stdioViewerURL = '/stdio.html?delivId=' + result.delivId + '&repoId=' + result.repoId + '&sha=' + result.commitSHA; let score: number | string = ''; + let scorePrepend = ''; score = result.scoreOverall; if (score === 100) { score = "100.00"; @@ -187,18 +188,19 @@ export class AdminResultsTab extends AdminPage { // two decimal places score = score.toFixed(2); // prepend space (not 100) - score = " " + score; + scorePrepend = " " + scorePrepend; if (result.scoreOverall < 10) { // prepend with extra space if < 10 - score = " " + score; + scorePrepend = " " + scorePrepend; } } + // ion-ios-help-outline const row: TableCell[] = [ { value: '', html: '' + stdioViewerURL + '">' }, { value: result.repoId, @@ -206,7 +208,7 @@ export class AdminResultsTab extends AdminPage { }, // {value: result.repoId, html: result.repoId}, {value: result.delivId, html: result.delivId}, - {value: result.scoreOverall, html: score}, + {value: result.scoreOverall, html: scorePrepend + score}, {value: result.state, html: result.state}, {value: ts, html: '' + tsString + ''} ]; From 71549445feff6038295cf647d062fa3c0d31913d Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 20:18:38 -0700 Subject: [PATCH 028/104] the neverending battle against log verbosity continues --- docs/tech-staff/operatingclassy.md | 14 ++++- packages/autotest/src/autotest/ClassPortal.ts | 58 ++++++++++--------- packages/common/Log.ts | 6 +- .../backend/src/controllers/GitHubActions.ts | 5 +- .../src/server/common/AutoTestRoutes.ts | 6 +- 5 files changed, 53 insertions(+), 36 deletions(-) diff --git a/docs/tech-staff/operatingclassy.md b/docs/tech-staff/operatingclassy.md index 75af8d8de..f7a23b733 100644 --- a/docs/tech-staff/operatingclassy.md +++ b/docs/tech-staff/operatingclassy.md @@ -21,6 +21,12 @@ docker network create --attachable --ip-range "172.28.5.0/24" --gateway "172.28. docker-compose build ``` +You can also build just a subset of classy, which can save time; the main modules that need rebuilding are: + +```bash +docker-compose build portal autotest +``` + ## Starting Classy Classy is a containerized application that requires containers are built before the application can be run. Building a container is necessary to make a copy of an image that Docker can run. @@ -66,12 +72,18 @@ docker-compose down ``` ## Restarting Classy -To restart Classy: +To restart Classy (this restarts the _current_ containers and is not sufficient if you are building new images): ```bash docker-compose restart ``` +If you're restarting after updating images, use: + +```bash +docker-compose up -d +``` + ## Viewing Classy Runtime Logs To view the logs while Classy is running: diff --git a/packages/autotest/src/autotest/ClassPortal.ts b/packages/autotest/src/autotest/ClassPortal.ts index a54236ddf..e61bb589f 100644 --- a/packages/autotest/src/autotest/ClassPortal.ts +++ b/packages/autotest/src/autotest/ClassPortal.ts @@ -118,8 +118,8 @@ export class ClassPortal implements IClassPortal { try { Log.info("ClassPortal::isStaff(..) - requesting from: " + url); const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), - headers: { + agent: new https.Agent({rejectUnauthorized: false}), + headers: { token: Config.getInstance().getProp(ConfigKey.autotestSecret) } }; @@ -127,6 +127,8 @@ export class ClassPortal implements IClassPortal { const res = await fetch(url, opts); Log.trace("ClassPortal::isStaff( " + userName + " ) - success; payload: " + res + "; took: " + Util.took(start)); const json: AutoTestAuthPayload = await res.json() as AutoTestAuthPayload; + Log.info("ClassPortal::isStaff( " + userName + " ) - success; isStaff: " + + json.success.isStaff + "; isAdmin: " + json.success.isAdmin); if (typeof json.success !== 'undefined') { return json.success; } else { @@ -148,8 +150,8 @@ export class ClassPortal implements IClassPortal { try { Log.info("ClassPortal::personId(..) - requesting from: " + url); const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), - headers: { + agent: new https.Agent({rejectUnauthorized: false}), + headers: { token: Config.getInstance().getProp(ConfigKey.autotestSecret) } }; @@ -175,14 +177,14 @@ export class ClassPortal implements IClassPortal { const start = Date.now(); const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), headers: { + agent: new https.Agent({rejectUnauthorized: false}), headers: { token: Config.getInstance().getProp(ConfigKey.autotestSecret) } }; - Log.info("ClassPortal::getConfiguration(..) - requesting from: " + url); + Log.trace("ClassPortal::getConfiguration(..) - requesting from: " + url); try { const res = await fetch(url, opts); - Log.info("ClassPortal::getConfiguration() - success; took: " + Util.took(start)); + Log.trace("ClassPortal::getConfiguration() - success; took: " + Util.took(start)); Log.trace("ClassPortal::getConfiguration() - success; payload:", res); const json: ClassyConfigurationPayload = await res.json() as ClassyConfigurationPayload; if (typeof json.success !== 'undefined') { @@ -202,14 +204,14 @@ export class ClassPortal implements IClassPortal { const start = Date.now(); const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), headers: { + agent: new https.Agent({rejectUnauthorized: false}), headers: { token: Config.getInstance().getProp(ConfigKey.autotestSecret) } }; - Log.info("ClassPortal::getContainerDetails(..) - requesting from: " + url); + Log.trace("ClassPortal::getContainerDetails(..) - requesting from: " + url); if (delivId === null || delivId === 'null') { - Log.info("ClassPortal::getContainerDetails(..) - skipping request; null delivId"); + Log.trace("ClassPortal::getContainerDetails(..) - skipping request; null delivId"); return null; } else { try { @@ -235,13 +237,13 @@ export class ClassPortal implements IClassPortal { const start = Date.now(); try { const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), - method: 'POST', - headers: { + agent: new https.Agent({rejectUnauthorized: false}), + method: 'POST', + headers: { "Content-Type": "application/json", - "token": Config.getInstance().getProp(ConfigKey.autotestSecret) + "token": Config.getInstance().getProp(ConfigKey.autotestSecret) }, - body: JSON.stringify(grade) + body: JSON.stringify(grade) }; Log.trace("ClassPortal::sendGrade(..) - sending to: " + url + '; delivId: ' + grade.delivId + @@ -312,13 +314,13 @@ export class ClassPortal implements IClassPortal { try { const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), - method: 'post', - headers: { + agent: new https.Agent({rejectUnauthorized: false}), + method: 'post', + headers: { "Content-Type": "application/json", - "token": Config.getInstance().getProp(ConfigKey.autotestSecret) + "token": Config.getInstance().getProp(ConfigKey.autotestSecret) }, - body: JSON.stringify(result) + body: JSON.stringify(result) }; Log.trace("ClassPortal::sendResult(..) - sending to: " + url + ' for delivId: ' + result.delivId + @@ -348,9 +350,9 @@ export class ClassPortal implements IClassPortal { try { const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), - method: 'get', - headers: {token: Config.getInstance().getProp(ConfigKey.autotestSecret)} + agent: new https.Agent({rejectUnauthorized: false}), + method: 'get', + headers: {token: Config.getInstance().getProp(ConfigKey.autotestSecret)} }; Log.info("ClassPortal::getResult(..) - requesting from: " + url); @@ -385,13 +387,13 @@ export class ClassPortal implements IClassPortal { try { const opts: RequestInit = { - agent: new https.Agent({ rejectUnauthorized: false }), - method: 'POST', - headers: { + agent: new https.Agent({rejectUnauthorized: false}), + method: 'POST', + headers: { "Content-Type": "application/json", - "token": Config.getInstance().getProp(ConfigKey.autotestSecret) + "token": Config.getInstance().getProp(ConfigKey.autotestSecret) }, - body: JSON.stringify(info) + body: JSON.stringify(info) }; const response = await fetch(url, opts); diff --git a/packages/common/Log.ts b/packages/common/Log.ts index 2ac96b03b..e48d46870 100644 --- a/packages/common/Log.ts +++ b/packages/common/Log.ts @@ -19,7 +19,7 @@ export default class Log { public static parseLogLevel(): LogLevel { try { - console.log("Log::parseLogLevel() - start; currently: " + Log.Level); + // console.log("Log::parseLogLevel() - start; currently: " + Log.Level); let valToSwitch = null; if (typeof Log.Level === "undefined") { // if undefined, use .env; otherwise re-parse value @@ -30,7 +30,7 @@ export default class Log { if (typeof valToSwitch !== "string") { LOG_LEVEL = Log.Level; - console.log("Log::parseLogLevel() - unchanged; current level: " + LOG_LEVEL); + // console.log("Log::parseLogLevel() - unchanged; current level: " + LOG_LEVEL); return LOG_LEVEL; } else { // if the value isn't a string, it must be a LogLevel already @@ -57,7 +57,7 @@ export default class Log { default: LOG_LEVEL = LogLevel.TRACE; } - console.log("Log::parseLogLevel() - log level: " + LOG_LEVEL); + // console.log("Log::parseLogLevel() - log level: " + LOG_LEVEL); Log.Level = LOG_LEVEL; return LOG_LEVEL; } diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index 4b1bc18e3..6644e712e 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -1008,7 +1008,7 @@ export class GitHubActions implements IGitHubActions { * @returns {Promise} */ public async getTeamMembers(teamName: string): Promise { - Log.info("GitHubAction::getTeamMembers( " + teamName + " ) - start"); + Log.trace("GitHubAction::getTeamMembers( " + teamName + " ) - start"); if (teamName === null) { throw new Error("GitHubAction::getTeamMembers( null ) - null team requested"); @@ -1043,7 +1043,8 @@ export class GitHubActions implements IGitHubActions { // ids.push(result.login); // } - Log.info("GitHubAction::getTeamMembers(..) - success; # results: " + ids.length + "; took: " + Util.took(start)); + Log.info("GitHubAction::getTeamMembers( " + teamName + " ) - success; # results: " + + ids.length + "; took: " + Util.took(start)); return ids; } catch (err) { diff --git a/packages/portal/backend/src/server/common/AutoTestRoutes.ts b/packages/portal/backend/src/server/common/AutoTestRoutes.ts index 141aa8182..2c1d8c743 100644 --- a/packages/portal/backend/src/server/common/AutoTestRoutes.ts +++ b/packages/portal/backend/src/server/common/AutoTestRoutes.ts @@ -193,6 +193,8 @@ export class AutoTestRoutes implements IREST { throw new Error('Invalid Grade Record: ' + validGradeRecord); } else { Log.info('AutoTestRouteHandler::atGrade(..) - repoId: ' + grade.repoId + + '; delivId: ' + grade.delivId + '; grade: ' + grade.score); + Log.trace('AutoTestRouteHandler::atGrade(..) - repoId: ' + grade.repoId + '; delivId: ' + grade.delivId + '; body: ' + JSON.stringify(grade)); const cc = new AdminController(new GitHubController(GitHubActions.getInstance())); const success = await cc.processNewAutoTestGrade(grade); @@ -286,13 +288,13 @@ export class AutoTestRoutes implements IREST { const ac = new AuthController(); const priv = await ac.personPriviliged(person); payload = {success: {personId: person.githubId, isStaff: priv.isStaff, isAdmin: priv.isAdmin}}; - Log.info('AutoTestRouteHandler::atIsStaff(..) - /isStaff/:githubId - done: ' + + Log.trace('AutoTestRouteHandler::atIsStaff(..) - /isStaff/:githubId - done: ' + JSON.stringify(payload) + "; took: " + Util.took(start)); res.send(200, payload); return next(true); } else { payload = {success: {personId: githubId, isStaff: false, isAdmin: false}}; - Log.info('AutoTestRouteHandler::atIsStaff(..) - /isStaff/:githubId - unknown person; result: ' + + Log.trace('AutoTestRouteHandler::atIsStaff(..) - /isStaff/:githubId - unknown person; result: ' + JSON.stringify(payload)); res.send(200, payload); return next(true); From 9f9d4438a9fd6d1a232fed2e4857d6a5e9dd2947 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 21:40:30 -0700 Subject: [PATCH 029/104] get path mappings working in autotest and improved in portal backend --- packages/autotest/src/autotest/ClassPortal.ts | 6 +- packages/autotest/tsconfig.json | 12 ++- .../backend/src/controllers/GitHubActions.ts | 2 +- .../src/server/common/AutoTestRoutes.ts | 82 +++++++++---------- .../test/controllers/GitHubActionSpec.ts | 34 ++++---- packages/portal/backend/tsconfig.json | 5 +- 6 files changed, 76 insertions(+), 65 deletions(-) diff --git a/packages/autotest/src/autotest/ClassPortal.ts b/packages/autotest/src/autotest/ClassPortal.ts index e61bb589f..53c0f9c3b 100644 --- a/packages/autotest/src/autotest/ClassPortal.ts +++ b/packages/autotest/src/autotest/ClassPortal.ts @@ -1,9 +1,9 @@ import * as https from "https"; import fetch, {RequestInit} from "node-fetch"; -import Config, {ConfigKey} from "../../../common/Config"; -import Log from "../../../common/Log"; -import {AutoTestResult} from "../../../common/types/AutoTestTypes"; +import Config, {ConfigKey} from "@common/Config"; +import Log from "@common/Log"; +import {AutoTestResult} from "@common/types/AutoTestTypes"; import {CommitTarget} from "../../../common/types/ContainerTypes"; import { AutoTestAuthPayload, diff --git a/packages/autotest/tsconfig.json b/packages/autotest/tsconfig.json index 62c53c03f..729da40e5 100644 --- a/packages/autotest/tsconfig.json +++ b/packages/autotest/tsconfig.json @@ -6,6 +6,15 @@ "removeComments": false, "preserveConstEnums": true, "sourceMap": true, + "baseUrl": ".", + "paths": { + "@common/*": [ + "../common/*" + ], + "@backend/*": [ + "../portal/backend/src/*" + ] + }, "lib": [ "es7", "es2017.object", @@ -17,8 +26,7 @@ }, "include": [ "src/**/*.ts", - "test/**/*.ts", - "../common/**/*.ts" + "test/**/*.ts" ], "exclude": [ "node_modules" diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index 6644e712e..9f30c1eb0 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -1049,7 +1049,7 @@ export class GitHubActions implements IGitHubActions { return ids; } catch (err) { Log.warn("GitHubAction::getTeamMembers(..) - ERROR: " + JSON.stringify(err)); - // just return empy [] rather than failing + // just return empty [] rather than failing return []; } } diff --git a/packages/portal/backend/src/server/common/AutoTestRoutes.ts b/packages/portal/backend/src/server/common/AutoTestRoutes.ts index 2c1d8c743..987c260fc 100644 --- a/packages/portal/backend/src/server/common/AutoTestRoutes.ts +++ b/packages/portal/backend/src/server/common/AutoTestRoutes.ts @@ -1,9 +1,9 @@ import fetch, {RequestInit} from "node-fetch"; import * as restify from 'restify'; -import Config, {ConfigKey} from "../../../../../common/Config"; -import Log from "../../../../../common/Log"; -import {AutoTestResult} from "../../../../../common/types/AutoTestTypes"; +import Config, {ConfigKey} from "@common/Config"; +import Log from "@common/Log"; +import {AutoTestResult} from "@common/types/AutoTestTypes"; import { AutoTestAuthPayload, AutoTestConfigPayload, @@ -13,19 +13,19 @@ import { AutoTestResultTransport, ClassyConfigurationPayload, Payload -} from "../../../../../common/types/PortalTypes"; -import Util from "../../../../../common/Util"; -import {AdminController} from "../../controllers/AdminController"; -import {AuthController} from "../../controllers/AuthController"; - -import {CommitTarget} from "../../../../../common/types/ContainerTypes"; -import {DatabaseController} from "../../controllers/DatabaseController"; -import {DeliverablesController} from "../../controllers/DeliverablesController"; -import {GitHubActions} from "../../controllers/GitHubActions"; -import {GitHubController} from "../../controllers/GitHubController"; -import {GradesController} from "../../controllers/GradesController"; -import {PersonController} from "../../controllers/PersonController"; -import {ResultsController} from "../../controllers/ResultsController"; +} from "@common/types/PortalTypes"; +import Util from "@common/Util"; +import {CommitTarget} from "@common/types/ContainerTypes"; + +import {AdminController} from "@backend/controllers/AdminController"; +import {AuthController} from "@backend/controllers/AuthController"; +import {DeliverablesController} from "@backend/controllers/DeliverablesController"; +import {GitHubActions} from "@backend/controllers/GitHubActions"; +import {GitHubController} from "@backend/controllers/GitHubController"; +import {GradesController} from "@backend/controllers/GradesController"; +import {PersonController} from "@backend/controllers/PersonController"; +import {ResultsController} from "@backend/controllers/ResultsController"; + import {Factory} from "../../Factory"; import IREST from "../IREST"; @@ -77,17 +77,17 @@ export class AutoTestRoutes implements IREST { Log.trace('AutoTestRouteHandler::atContainerDetails(..) - name: ' + name + '; delivId: ' + delivId); const dc = new DeliverablesController(); - dc.getDeliverable(delivId).then(function(deliv) { + dc.getDeliverable(delivId).then(function (deliv) { if (deliv !== null) { const at: AutoTestConfigTransport = { - dockerImage: deliv.autotest.dockerImage, - studentDelay: deliv.autotest.studentDelay, - maxExecTime: deliv.autotest.maxExecTime, + dockerImage: deliv.autotest.dockerImage, + studentDelay: deliv.autotest.studentDelay, + maxExecTime: deliv.autotest.maxExecTime, regressionDelivIds: deliv.autotest.regressionDelivIds, - custom: deliv.autotest.custom, - openTimestamp: deliv.openTimestamp, - closeTimestamp: deliv.closeTimestamp, - lateAutoTest: deliv.lateAutoTest + custom: deliv.autotest.custom, + openTimestamp: deliv.openTimestamp, + closeTimestamp: deliv.closeTimestamp, + lateAutoTest: deliv.lateAutoTest }; payload = {success: at}; Log.trace('AutoTestRouteHandler::atContainerDetails(..) - /at/container/:delivId - done; took: ' + Util.took(start)); @@ -97,7 +97,7 @@ export class AutoTestRoutes implements IREST { // This is more like a warning; if a deliverable isn't configured this is going to happen return AutoTestRoutes.handleError(400, 'Could not retrieve container details for delivId: ' + delivId, res, next); } - }).catch(function(err) { + }).catch(function (err) { return AutoTestRoutes.handleError(400, 'Could not retrieve container details.', res, next); }); } @@ -120,11 +120,11 @@ export class AutoTestRoutes implements IREST { let defaultDeliverable: string | null = null; Log.trace('AutoTestRouteHandler::atConfiguration(..) - cc; took: ' + Util.took(start)); - cc.getCourse().then(function(course) { + cc.getCourse().then(function (course) { defaultDeliverable = course.defaultDeliverableId; Log.trace('AutoTestRouteHandler::atConfiguration(..) - default: ' + defaultDeliverable + '; took: ' + Util.took(start)); return cc.getDeliverables(); - }).then(function(deliverables) { + }).then(function (deliverables) { const delivIds = []; for (const deliv of deliverables) { delivIds.push(deliv.id); @@ -134,7 +134,7 @@ export class AutoTestRoutes implements IREST { Log.trace('AutoTestRouteHandler::atConfiguration(..) - /at - done; took: ' + Util.took(start)); res.send(200, payload); return next(true); - }).catch(function(err) { + }).catch(function (err) { return AutoTestRoutes.handleError(400, 'Error retrieving backend configuration.', res, next); }); } @@ -175,12 +175,12 @@ export class AutoTestRoutes implements IREST { } else { const gradeRecord: AutoTestGradeTransport = req.body; - AutoTestRoutes.performPostGrade(gradeRecord).then(function(saved: any) { + AutoTestRoutes.performPostGrade(gradeRecord).then(function (saved: any) { payload = {success: {success: saved}}; Log.trace('AutoTestRouteHandler::atGrade(..) - done; took: ' + Util.took(start)); res.send(200, payload); return next(true); - }).catch(function(err) { + }).catch(function (err) { return AutoTestRoutes.handleError(400, 'Failed to receive grade; ERROR: ' + err.message, res, next); }); } @@ -225,12 +225,12 @@ export class AutoTestRoutes implements IREST { } else { const resultRecord: AutoTestResultTransport = req.body; // Log.trace('AutoTestRouteHandler::atPostResult(..) - body: ' + JSON.stringify(resultRecord)); - AutoTestRoutes.performPostResult(resultRecord).then(function() { + AutoTestRoutes.performPostResult(resultRecord).then(function () { payload = {success: {message: 'Result received'}}; Log.trace('AutoTestRouteHandler::atPostResult(..) - done; took: ' + Util.took(start)); res.send(200, payload); return next(true); - }).catch(function(err) { + }).catch(function (err) { return AutoTestRoutes.handleError(400, 'Error processing result: ' + err.message, res, next); }); } @@ -315,7 +315,7 @@ export class AutoTestRoutes implements IREST { const githubId = req.params.githubId; const pc = new PersonController(); - pc.getGitHubPerson(githubId).then(function(person) { + pc.getGitHubPerson(githubId).then(function (person) { if (person !== null) { Log.info('AutoTestRouteHandler::atPersonId(..) - personId: ' + person.id + '; githubId: ' + githubId + "; took: " + Util.took(start)); @@ -325,7 +325,7 @@ export class AutoTestRoutes implements IREST { } else { return AutoTestRoutes.handleError(404, 'Invalid person id: ' + githubId, res, next); } - }).catch(function(err) { + }).catch(function (err) { return AutoTestRoutes.handleError(404, 'Invalid person id: ' + githubId, res, next); }); } @@ -347,7 +347,7 @@ export class AutoTestRoutes implements IREST { Log.info('AutoTestRouteHandler::atGetResult(..) - delivId: ' + delivId + '; repoId: ' + repoId + '; sha: ' + sha); const rc = new ResultsController(); - rc.getResult(delivId, repoId, sha).then(function(result: AutoTestResult) { + rc.getResult(delivId, repoId, sha).then(function (result: AutoTestResult) { if (result !== null) { payload = {success: [result]}; } else { @@ -355,7 +355,7 @@ export class AutoTestRoutes implements IREST { } res.send(200, payload); return next(true); - }).catch(function(err) { + }).catch(function (err) { return AutoTestRoutes.handleError(400, 'Error retrieving result record: ' + err.message, res, next); }); } @@ -393,10 +393,10 @@ export class AutoTestRoutes implements IREST { Log.trace('AutoTestRouteHandler::githubWebhook(..) - start'); const start = Date.now(); - AutoTestRoutes.handleWebhook(req).then(function(succ) { + AutoTestRoutes.handleWebhook(req).then(function (succ) { Log.info('AutoTestRouteHandler::githubWebhook(..) - success; took: ' + Util.took(start)); res.send(200, succ); - }).catch(function(err) { + }).catch(function (err) { Log.error('AutoTestRouteHandler::githubWebhook(..) - ERROR: ' + err.message + "; took: " + Util.took(start)); if (err.message && err.message.indexOf("hang up") >= 0) { Log.error('AutoTestRouteHandler::githubWebhook(..) - ERROR: handling hangup; ending response'); @@ -420,9 +420,9 @@ export class AutoTestRoutes implements IREST { const atHost = config.getProp(ConfigKey.autotestUrl); const url = atHost + ':' + config.getProp(ConfigKey.autotestPort) + '/githubWebhook'; const options: RequestInit = { - method: 'POST', + method: 'POST', headers: JSON.parse(headers), // use GitHub's headers - body: JSON.stringify(req.body) + body: JSON.stringify(req.body) }; const res = await fetch(url, options); if (res.ok) { @@ -483,7 +483,7 @@ export class AutoTestRoutes implements IREST { const headers = JSON.stringify(req.headers); const options: RequestInit = { method: 'POST', - body: JSON.stringify(req.body), + body: JSON.stringify(req.body), headers: JSON.parse(headers) }; diff --git a/packages/portal/backend/test/controllers/GitHubActionSpec.ts b/packages/portal/backend/test/controllers/GitHubActionSpec.ts index 55a5a1023..7e54add61 100644 --- a/packages/portal/backend/test/controllers/GitHubActionSpec.ts +++ b/packages/portal/backend/test/controllers/GitHubActionSpec.ts @@ -1,17 +1,17 @@ import {expect} from "chai"; import "mocha"; -import Config, {ConfigKey} from "../../../../common/Config"; -import Log from "../../../../common/Log"; -import {Test} from "../../../../common/TestHarness"; -import Util from "../../../../common/Util"; -import {DatabaseController} from "../../src/controllers/DatabaseController"; - -import {DeliverablesController} from "../../src/controllers/DeliverablesController"; -import {GitHubActions, IGitHubActions} from "../../src/controllers/GitHubActions"; -import {PersonController} from "../../src/controllers/PersonController"; -import {RepositoryController} from "../../src/controllers/RepositoryController"; -import {TeamController} from "../../src/controllers/TeamController"; +import Config, {ConfigKey} from "@common/Config"; +import Log from "@common/Log"; +import {Test} from "@common/TestHarness"; +import Util from "@common/Util"; + +import {DatabaseController} from "@backend/controllers/DatabaseController"; +import {DeliverablesController} from "@backend/controllers/DeliverablesController"; +import {GitHubActions, IGitHubActions} from "@backend/controllers/GitHubActions"; +import {PersonController} from "@backend/controllers/PersonController"; +import {RepositoryController} from "@backend/controllers/RepositoryController"; +import {TeamController} from "@backend/controllers/TeamController"; import '../GlobalSpec'; @@ -831,13 +831,13 @@ describe("GitHubActions", () => { expect(del).to.be.true; }).timeout(TIMEOUT * 10); - function getProjectPrefix(): string { - return "TEST__X__secap_"; - } + // function getProjectPrefix(): string { + // return "TEST__X__secap_"; + // } - function getTeamPrefix() { - return "TEST__X__t_"; - } + // function getTeamPrefix() { + // return "TEST__X__t_"; + // } async function deleteStale(): Promise { Log.test('GitHubActionSpec::deleteStale() - start'); diff --git a/packages/portal/backend/tsconfig.json b/packages/portal/backend/tsconfig.json index 81a761124..d8afe32c3 100644 --- a/packages/portal/backend/tsconfig.json +++ b/packages/portal/backend/tsconfig.json @@ -9,8 +9,11 @@ "baseUrl": ".", "paths": { "@backend/*": [ - "../../portal/backend/src/*" + "./src/*" ], + "@common/*": [ + "../../common/*" + ] }, "lib": [ "es7", From d45018f8f4b348da726f7462f3532efd7f6a55f2 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 21:57:10 -0700 Subject: [PATCH 030/104] collect runtime info so we can work on #363 --- .../autotest/src/github/GitHubAutoTest.ts | 45 ++++++++----------- packages/autotest/src/github/GitHubUtil.ts | 38 +++++++++++----- packages/autotest/test/GitHubUtilSpec.ts | 9 ++-- packages/common/types/ContainerTypes.ts | 2 +- 4 files changed, 51 insertions(+), 43 deletions(-) diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index dfc71eebe..2e8f1532d 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -1,16 +1,16 @@ import * as Docker from "dockerode"; -import Config, {ConfigKey} from "../../../common/Config"; -import Log from "../../../common/Log"; -import {AutoTestResult, IFeedbackGiven} from "../../../common/types/AutoTestTypes"; -import {CommitTarget, ContainerInput} from "../../../common/types/ContainerTypes"; +import Config, {ConfigKey} from "@common/Config"; +import Log from "@common/Log"; +import {AutoTestResult, IFeedbackGiven} from "@common/types/AutoTestTypes"; +import {CommitTarget, ContainerInput} from "@common/types/ContainerTypes"; import { AutoTestAuthTransport, AutoTestConfigTransport, AutoTestResultTransport, ClassyConfigurationTransport -} from "../../../common/types/PortalTypes"; -import Util from "../../../common/Util"; +} from "@common/types/PortalTypes"; +import Util from "@common/Util"; import {AutoTest} from "../autotest/AutoTest"; import {IClassPortal} from "../autotest/ClassPortal"; import {IDataStore} from "../autotest/DataStore"; @@ -21,7 +21,7 @@ export interface IGitHubTestManager { /** * Handles a push event from GitHub. Will place job on queue. * - * @param {IPushEvent} push + * @param {CommitTarget} push */ handlePushEvent(push: CommitTarget): void; @@ -30,7 +30,7 @@ export interface IGitHubTestManager { * express queue if appropriate. When job is complete, it will * comment back automatically for the user. * - * @param {ICommentEvent} comment + * @param {CommitTarget} comment */ handleCommentEvent(comment: CommitTarget): void; } @@ -150,7 +150,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { /** * - * @param {ICommentEvent} info + * @param {CommitTarget} info * @returns {boolean} true if the preconditions are met; false otherwise */ private async checkCommentPreconditions(info: CommitTarget): Promise { @@ -163,7 +163,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { Log.info("GitHubAutoTest::checkCommentPreconditions(..) - for: " + info.personId + "; commit: " + info.commitSHA); - // ignore messges made by the bot, unless they are #force + // ignore messages made by the bot, unless they are #force if (info.personId === Config.getInstance().getProp(ConfigKey.botName)) { if (typeof info.flags !== 'undefined' && info.flags.indexOf("#force") >= 0) { @@ -220,7 +220,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { return false; } - // reject #silent requests by requestors that are not admins or staff + // reject #silent requests by requesters that are not admins or staff if (info.flags.indexOf("#silent") >= 0) { Log.warn("GitHubAutoTest::checkCommentPreconditions(..) - ignored, student use of #silent"); const msg = "Only admins can use the #silent flag."; @@ -228,15 +228,6 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { await this.postToGitHub(info, {url: info.postbackURL, message: msg}); return false; } - - // reject requests that include schedule AND unschedule (as this doesn't make sense as a request) - if (info.flags.indexOf("#schedule") >= 0 && info.flags.indexOf("#unschedule") >= 0) { - Log.warn("GitHubAutoTest::checkCommentPreconditions(..) - " + - "ignored, undefined behaviour: both #schedule AND #unschedule."); - const msg = "Please choose either #schedule or #unschedule. Both commands cannot be used in the same request."; - await this.postToGitHub(info, {url: info.postbackURL, message: msg}); - return false; - } } // reject requests for executing deliverables that are not yet open @@ -293,7 +284,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { Log.info("GitHubAutoTest::processCommentExists(..) - handling request for: " + info.personId + "; delivId: " + info.delivId + "; commit: " + info.commitURL); - const containerDetails = await this.classPortal.getContainerDetails(res.delivId); + // const containerDetails = await this.classPortal.getContainerDetails(res.delivId); const msg = await this.classPortal.formatFeedback(res); await this.postToGitHub(info, {url: info.postbackURL, message: msg}); @@ -351,7 +342,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { if (pe === null) { Log.warn("GitHubAutoTest::processCommentNew(..) - push event was not present; adding now. URL: " + info.commitURL + "; for: " + info.personId + "; SHA: " + info.commitSHA); - // store this pushevent for consistency in case we need it for anything else later + // store this push event for consistency in case we need it for anything else later await this.dataStore.savePush(info); // NEXT: add cloneURL to commentEvent (should be in github payload) } msg = "This commit has been queued for processing against " + info.delivId + "."; @@ -461,7 +452,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { * If build is finished: * * post back results if previously requested * * post back results if requested by TA - * * post back results if rate limiting check passes (and record fedback given) + * * post back results if rate limiting check passes (and record feedback given) * * post back warning if rate limiting check fails */ public async handleCommentEvent(info: CommitTarget): Promise { @@ -573,7 +564,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { const standardFeedbackRequested: CommitTarget = await this.getRequester(data.commitURL, delivId, 'standard'); const checkFeedbackRequested: CommitTarget = await this.getRequester(data.commitURL, delivId, 'check'); - const containerConfig = await this.classPortal.getContainerDetails(delivId); + // const containerConfig = await this.classPortal.getContainerDetails(delivId); const personId = data.input.target.personId; const feedbackDelay: string | null = await this.requestFeedbackDelay(delivId, personId, data.input.target.timestamp); @@ -634,7 +625,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { /** * Check to see if the current user is allowed to make a result request * - * Null means yes, string will contain how long (in a human readable format). + * Null means yes, string will contain how long (in a human-readable format). * * @param delivId * @param userName @@ -693,7 +684,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { /** * Saves pushInfo in its own table in the database, in case we need to refer to it later * - * @param {IContainerInput} info + * @param {CommitTarget} info */ private async savePushInfo(info: CommitTarget) { try { @@ -743,6 +734,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { * @param userName * @param timestamp * @param commitURL + * @param kind */ private async saveFeedbackGiven(delivId: string, userName: string, timestamp: number, commitURL: string, kind: string): Promise { try { @@ -766,6 +758,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { * * @param commitURL * @param delivId + * @param kind */ private async getRequester(commitURL: string, delivId: string, kind: string): Promise { try { diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index 4704c922e..fafe0226c 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -1,11 +1,11 @@ import fetch, {RequestInit} from 'node-fetch'; -import Config, {ConfigKey} from "../../../common/Config"; -import Log from "../../../common/Log"; +import Config, {ConfigKey} from "@common/Config"; +import {CommitTarget} from "@common/types/ContainerTypes"; +import Log from "@common/Log"; +import {AutoTestAuthTransport} from "@common/types/PortalTypes"; -import {AutoTestAuthTransport} from "../../../common/types/PortalTypes"; import {ClassPortal, IClassPortal} from "../autotest/ClassPortal"; -import {CommitTarget} from "../../../common/types/ContainerTypes"; export interface IGitHubMessage { /** @@ -30,6 +30,7 @@ export class GitHubUtil { * The '#' is required, the 'd' is required, and a number is required. * * @param message + * @param {string[]} delivIds * @returns {string | null} */ public static parseDeliverableFromComment(message: any, delivIds: string[]): string | null { @@ -87,11 +88,17 @@ export class GitHubUtil { * Throws exception if something goes wrong. * * @param payload - * @returns {ICommentEvent} + * @returns {Promise} */ public static async processComment(payload: any): Promise { try { Log.info("GitHubUtil::processComment(..) - start"); + + // NOTE: this will be released once #363 is fixed + // need org name to be attached to CommitTarget to differentiate + // between repos in different terms + Log.info("GitHubUtil::processComment(..) - payload: " + JSON.stringify(payload)); + const commitSHA = payload.comment.commit_id; let commitURL = payload.comment.html_url; // this is the comment Url commitURL = commitURL.substr(0, commitURL.lastIndexOf("#")); // strip off the comment reference @@ -100,7 +107,7 @@ export class GitHubUtil { // NEXT: need cloneURL const cloneURL = String(payload.repository.clone_url); - const requestor = String(payload.comment.user.login); // .toLowerCase(); + const requester = String(payload.comment.user.login); // .toLowerCase(); const message = payload.comment.body; Log.trace("GitHubUtil::processComment(..) - 1"); @@ -124,7 +131,7 @@ export class GitHubUtil { const timestamp = Date.now(); // set timestamp to the time the commit was made // need to get this from portal backend (this is a gitHubId, not a personId) - const personResponse = await cp.getPersonId(requestor); // NOTE: this returns Person.id, id, not Person.gitHubId! + const personResponse = await cp.getPersonId(requester); // NOTE: this returns Person.id, id, not Person.gitHubId! const personId = personResponse.personId; let adminRequest = false; @@ -163,7 +170,7 @@ export class GitHubUtil { } } - Log.info("GitHubUtil.processComment(..) - who: " + requestor + "; repoId: " + + Log.info("GitHubUtil.processComment(..) - who: " + requester + "; repoId: " + repoId + "; botMentioned: " + botMentioned + "; message: " + msg); Log.trace("GitHubUtil::processComment(..) - done; commentEvent:", commentEvent); @@ -186,12 +193,19 @@ export class GitHubUtil { * * Returns null for push operations we do not need to handle (like branch deletion). * - * @param payload - * @returns {IPushEvent} + * @param {any} payload + * @param {IClassPortal} portal + * @returns {CommitTarget | null} null for pushes that should be ignored */ public static async processPush(payload: any, portal: IClassPortal): Promise { try { Log.trace("GitHubUtil::processPush(..) - start"); + + // NOTE: this will be released once #363 is fixed + // need org name to be attached to CommitTarget to differentiate + // between repos in different terms + Log.info("GitHubUtil::processPush(..) - payload: " + JSON.stringify(payload)); + const repo = payload.repository.name; const projectURL = payload.repository.html_url; const cloneURL = payload.repository.clone_url; @@ -238,8 +252,8 @@ export class GitHubUtil { repoId: repo, botMentioned: false, // not explicitly invoked adminRequest: false, // all pushes are treated equally - personId: pusher?.personId ?? null, - kind: 'push', + personId: pusher?.personId ?? null, + kind: 'push', cloneURL, commitSHA, commitURL, diff --git a/packages/autotest/test/GitHubUtilSpec.ts b/packages/autotest/test/GitHubUtilSpec.ts index 42cc1d49d..5994aef5d 100644 --- a/packages/autotest/test/GitHubUtilSpec.ts +++ b/packages/autotest/test/GitHubUtilSpec.ts @@ -1,10 +1,11 @@ import {expect} from "chai"; import "mocha"; -import Config from "../../common/Config"; -import Log from "../../common/Log"; -import {Test} from "../../common/TestHarness"; -import Util from "../../common/Util"; +import Config from "@common/Config"; +import Log from "@common/Log"; +// noinspection ES6UnusedImports +import {Test} from "@common/TestHarness"; // included intentionally (loads TestHarness first) +import Util from "@common/Util"; import {GitHubUtil} from "../src/github/GitHubUtil"; // const loadFirst = require('./GlobalSpec'); import "./GlobalSpec"; diff --git a/packages/common/types/ContainerTypes.ts b/packages/common/types/ContainerTypes.ts index fbd10545a..93cd3bd50 100644 --- a/packages/common/types/ContainerTypes.ts +++ b/packages/common/types/ContainerTypes.ts @@ -117,7 +117,7 @@ export interface GradeReport { /** * Description of attachments that are saved in files on disk. This - * helps minimize database size making it easier to backup and much + * helps minimize database size making it easier to back up and much * quicker to search and traverse (especially over the network). */ export interface Attachment { From 20c1063f9b2bea8ccd1207f25ad803dc448e14bc Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 22:29:54 -0700 Subject: [PATCH 031/104] try to get code to compile on prod again (and add the orgId to comment and push events) --- package.json | 4 +--- packages/autotest/package.json | 4 ++-- packages/autotest/src/github/GitHubAutoTest.ts | 18 ++++++++++++++++++ packages/autotest/src/github/GitHubUtil.ts | 7 +++++++ packages/common/types/ContainerTypes.ts | 1 + 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c7a22749f..5423fb756 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,6 @@ "lintOLD": "tslint -c tslint.json 'packages/portal/backend/src/**/*.ts' 'packages/portal/backend/test/**/*.ts' 'packages/portal/frontend/src/**/*.ts' 'packages/portal/frontend/test/**/*.ts' 'packages/autotest/src/**/*.ts' 'packages/autotest/test/**/*.ts'", "lint": "tslint --project tsconfig.json", "test": "mocha --require=dotenv/config --require ts-node/register --timeout 10000", - "cover": "nyc --reporter text --reporter html yarn run test", - "run:dev": "nohup node ./src/AutoTestDaemon.js 310 &> nohup.out &", - "run:prod": "LOG_LEVEL=INFO nohup node ./src/AutoTestDaemon.js 310 &> nohup.out &" + "cover": "nyc --reporter text --reporter html yarn run test" } } diff --git a/packages/autotest/package.json b/packages/autotest/package.json index de6889961..778ec4a60 100644 --- a/packages/autotest/package.json +++ b/packages/autotest/package.json @@ -43,8 +43,8 @@ "cover": "nyc --reporter text --reporter html ./node_modules/mocha/bin/mocha --require=dotenv/config --require ts-node/register --timeout 10000 --exit", "coverCI": "./node_modules/.bin/nyc --reporter html --report-dir ../../testOutput/autotest/coverage --reporter=text-lcov yarn run testCI", "coveralls": "./node_modules/.bin/nyc report --report-dir ../../testOutput/autotest/coverage --reporter=text-lcov | ./node_modules/coveralls/bin/coveralls.js", - "run:dev": "nohup node ./src/AutoTestDaemon.js 310 &> nohup.out &", - "run:prod": "LOG_LEVEL=INFO nohup node ./src/AutoTestDaemon.js 310 &> nohup.out &" + "run:dev": "LOG_LEVEL=TRACE nohup node --require tsconfig-paths/register ./src/AutoTestDaemon.js &> nohup.out &", + "run:prod": "LOG_LEVEL=INFO nohup node --require tsconfig-paths/register ./src/AutoTestDaemon.js &> nohup.out &" }, "devDependencies": { "@types/dockerode": "^2.5.9" diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index 2e8f1532d..7b4e7c0a8 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -61,6 +61,13 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { return false; } + const org = Config.getInstance().getProp(ConfigKey.org); + if (org !== info.orgId) { + Log.warn("GitHubAutoTest::handlePushEvent(..) - ignored, org: " + info.orgId + + " does not match current course: " + org); + return false; + } + Log.info("GitHubAutoTest::handlePushEvent(..) - start; commit: " + info.commitSHA); const start = Date.now(); await this.savePushInfo(info); @@ -200,6 +207,17 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { return false; } + const org = Config.getInstance().getProp(ConfigKey.org); + if (org !== info.orgId) { + Log.warn("GitHubAutoTest::checkCommentPreconditions(..) - ignored, org: " + info.orgId + + " does not match current course: " + org); + + // no deliverable, give warning and abort + const msg = "This commit appears to be from a prior version of the course; AutoTest cancelled."; + await this.postToGitHub(info, {url: info.postbackURL, message: msg}); + return false; + } + // TODO: invalid personId // TODO: invalid repoId diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index fafe0226c..34ac55045 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -107,6 +107,9 @@ export class GitHubUtil { // NEXT: need cloneURL const cloneURL = String(payload.repository.clone_url); + const org = payload.repository.full_name.substr(0, + payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); + const requester = String(payload.comment.user.login); // .toLowerCase(); const message = payload.comment.body; @@ -211,6 +214,9 @@ export class GitHubUtil { const cloneURL = payload.repository.clone_url; const ref = payload.ref; const pusher = await new ClassPortal().getPersonId(payload.pusher.name); + const org = payload.repository.full_name.substr(0, + payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); + Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; projectURL: " + projectURL + "; ref: " + ref); if (payload.deleted === true && payload.head_commit === null) { @@ -250,6 +256,7 @@ export class GitHubUtil { const pushEvent: CommitTarget = { delivId: backendConfig.defaultDeliverable, repoId: repo, + orgId: org, botMentioned: false, // not explicitly invoked adminRequest: false, // all pushes are treated equally personId: pusher?.personId ?? null, diff --git a/packages/common/types/ContainerTypes.ts b/packages/common/types/ContainerTypes.ts index 93cd3bd50..f135af889 100644 --- a/packages/common/types/ContainerTypes.ts +++ b/packages/common/types/ContainerTypes.ts @@ -42,6 +42,7 @@ export interface CommitTarget { */ delivId: string; repoId: string; + orgId?: string; adminRequest: boolean; // true if requested by admin or staff botMentioned: boolean; // true if explicitly mentioned From 95cd7d7da4fb58191937cea6702f2f5b2dbc947f Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 22:33:01 -0700 Subject: [PATCH 032/104] fix autotest docker launch command --- packages/autotest/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/autotest/Dockerfile b/packages/autotest/Dockerfile index 013fad513..73701024d 100644 --- a/packages/autotest/Dockerfile +++ b/packages/autotest/Dockerfile @@ -16,6 +16,6 @@ RUN yarn install --pure-lockfile --non-interactive --ignore-scripts \ && yarn tsc --sourceMap false \ && chmod -R a+rx /app -CMD ["node", "/app/packages/autotest/src/AutoTestDaemon.js"] +CMD ["node", "--require", "/app/node_modules/tsconfig-paths/register", "/app/packages/autotest/src/AutoTestDaemon.js"] From 4bb65fd6adb8e209b77caea5e95cf052baae13f8 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 22:47:10 -0700 Subject: [PATCH 033/104] don't cancel based on org name for now --- packages/autotest/src/github/GitHubAutoTest.ts | 14 ++++++++------ packages/autotest/src/github/GitHubUtil.ts | 4 ++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index 7b4e7c0a8..da71ec04f 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -62,10 +62,11 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } const org = Config.getInstance().getProp(ConfigKey.org); - if (org !== info.orgId) { + Log.info("GitHubAutoTest::handlePushEvent(..) - org: " + org + "; push org: " + info.orgId); + if (typeof org !== "undefined" && typeof info.orgId !== "undefined" && org !== info.orgId) { Log.warn("GitHubAutoTest::handlePushEvent(..) - ignored, org: " + info.orgId + " does not match current course: " + org); - return false; + // return false; } Log.info("GitHubAutoTest::handlePushEvent(..) - start; commit: " + info.commitSHA); @@ -208,14 +209,15 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { } const org = Config.getInstance().getProp(ConfigKey.org); - if (org !== info.orgId) { + Log.info("GitHubAutoTest::checkCommentPreconditions(..) - org: " + org + "; comment org: " + info.orgId); + if (typeof org !== "undefined" && typeof info.orgId !== "undefined" && org !== info.orgId) { Log.warn("GitHubAutoTest::checkCommentPreconditions(..) - ignored, org: " + info.orgId + " does not match current course: " + org); // no deliverable, give warning and abort - const msg = "This commit appears to be from a prior version of the course; AutoTest cancelled."; - await this.postToGitHub(info, {url: info.postbackURL, message: msg}); - return false; + // const msg = "This commit appears to be from a prior version of the course; AutoTest cancelled."; + // await this.postToGitHub(info, {url: info.postbackURL, message: msg}); + // return false; } // TODO: invalid personId diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index 34ac55045..8bd21d9ca 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -109,6 +109,8 @@ export class GitHubUtil { const cloneURL = String(payload.repository.clone_url); const org = payload.repository.full_name.substr(0, payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); + Log.info("GitHubUtil::processComment(..) - full_name: " + payload.repository.full_name + + "; name: " + payload.repository.name + "; org: " + org); const requester = String(payload.comment.user.login); // .toLowerCase(); const message = payload.comment.body; @@ -216,6 +218,8 @@ export class GitHubUtil { const pusher = await new ClassPortal().getPersonId(payload.pusher.name); const org = payload.repository.full_name.substr(0, payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); + Log.info("GitHubUtil::processPush(..) - full_name: " + payload.repository.full_name + + "; name: " + payload.repository.name + "; org: " + org); Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; projectURL: " + projectURL + "; ref: " + ref); From 26ade2b0f3bab9e6b621b9a07f075c6c9f2adf56 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 22:59:38 -0700 Subject: [PATCH 034/104] remove #363 logging; feature seems to work but two return statements are commented out right now for safety. can enable once the logs don't show false positives --- packages/autotest/src/github/GitHubAutoTest.ts | 2 ++ packages/autotest/src/github/GitHubUtil.ts | 15 +++------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index da71ec04f..9349e83c5 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -66,6 +66,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { if (typeof org !== "undefined" && typeof info.orgId !== "undefined" && org !== info.orgId) { Log.warn("GitHubAutoTest::handlePushEvent(..) - ignored, org: " + info.orgId + " does not match current course: " + org); + // TODO: turn statement on to fix #363 // return false; } @@ -215,6 +216,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { " does not match current course: " + org); // no deliverable, give warning and abort + // TODO: turn statements below on to fix #363 // const msg = "This commit appears to be from a prior version of the course; AutoTest cancelled."; // await this.postToGitHub(info, {url: info.postbackURL, message: msg}); // return false; diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index 8bd21d9ca..b297950d2 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -94,11 +94,6 @@ export class GitHubUtil { try { Log.info("GitHubUtil::processComment(..) - start"); - // NOTE: this will be released once #363 is fixed - // need org name to be attached to CommitTarget to differentiate - // between repos in different terms - Log.info("GitHubUtil::processComment(..) - payload: " + JSON.stringify(payload)); - const commitSHA = payload.comment.commit_id; let commitURL = payload.comment.html_url; // this is the comment Url commitURL = commitURL.substr(0, commitURL.lastIndexOf("#")); // strip off the comment reference @@ -107,10 +102,10 @@ export class GitHubUtil { // NEXT: need cloneURL const cloneURL = String(payload.repository.clone_url); - const org = payload.repository.full_name.substr(0, + const orgId = payload.repository.full_name.substr(0, payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); Log.info("GitHubUtil::processComment(..) - full_name: " + payload.repository.full_name + - "; name: " + payload.repository.name + "; org: " + org); + "; name: " + payload.repository.name + "; org: " + orgId); const requester = String(payload.comment.user.login); // .toLowerCase(); const message = payload.comment.body; @@ -155,6 +150,7 @@ export class GitHubUtil { const commentEvent: CommitTarget = { delivId, repoId, + orgId, botMentioned, commitSHA, commitURL, @@ -206,11 +202,6 @@ export class GitHubUtil { try { Log.trace("GitHubUtil::processPush(..) - start"); - // NOTE: this will be released once #363 is fixed - // need org name to be attached to CommitTarget to differentiate - // between repos in different terms - Log.info("GitHubUtil::processPush(..) - payload: " + JSON.stringify(payload)); - const repo = payload.repository.name; const projectURL = payload.repository.html_url; const cloneURL = payload.repository.clone_url; From d49708b53b2f89ac804027dc735c30d407843076 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Fri, 23 Sep 2022 23:15:15 -0700 Subject: [PATCH 035/104] handle an unusual edge case where a comment could sit on the queue until another push event would tick it. it would _queue_ right, but would not _tick_. --- packages/autotest/src/github/GitHubAutoTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index 9349e83c5..be739f9e1 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -539,6 +539,8 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { info.adminRequest = false; await this.handleCommentStudent(info, res); } + + this.tick(); // make sure the queues have ticked after the comment has been processed Log.trace("GitHubAutoTest::handleCommentEvent(..) - done; took: " + Util.took(start)); } From 68f6d692b4e09670605d210c8d1d85c29cef6574 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 10:41:54 -0700 Subject: [PATCH 036/104] fix #370; keep headers visible for long tables --- packages/portal/frontend/html/style.css | 6 +++ .../frontend/src/app/util/SortableTable.ts | 45 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/portal/frontend/html/style.css b/packages/portal/frontend/html/style.css index bada6dea5..968065f0b 100644 --- a/packages/portal/frontend/html/style.css +++ b/packages/portal/frontend/html/style.css @@ -120,6 +120,12 @@ Dashboard Page user-select: text !important; } +.sortableTable th { + background-color: white; + position: sticky; + top: 0; +} + .selectable { user-select: text !important; } diff --git a/packages/portal/frontend/src/app/util/SortableTable.ts b/packages/portal/frontend/src/app/util/SortableTable.ts index 1d16c2ddf..fa9b259a2 100644 --- a/packages/portal/frontend/src/app/util/SortableTable.ts +++ b/packages/portal/frontend/src/app/util/SortableTable.ts @@ -121,7 +121,7 @@ export class SortableTable { const ths = div.getElementsByTagName('th'); const thsArray = Array.prototype.slice.call(ths, 0); for (const th of thsArray) { - th.onclick = function() { + th.onclick = function () { const colName = this.getAttribute('col'); that.sort(colName); }; @@ -131,6 +131,17 @@ export class SortableTable { } this.attachDownload(); + + setTimeout(() => { + Log.info("SortableTable::generate() - updating table height; div: " + this.divName); + this.updateTableHeight(); + }, 100); + + // need to update the viewport so sticky headers keep working after resize events + window.addEventListener('resize', (evt) => { + Log.info("SortableTable::generate()::resize - div: " + this.divName); + this.updateTableHeight(); + }, true); } private startTable() { @@ -215,7 +226,7 @@ export class SortableTable { Log.trace('SortableTable::sort() - col: ' + sortHead.id + '; down: ' + sortHead.sortDown + '; mult: ' + mult + '; index: ' + sortIndex); - this.rows = this.rows.sort(function(a, b) { + this.rows = this.rows.sort(function (a, b) { const aVal = a[sortIndex].value; const bVal = b[sortIndex].value; @@ -358,4 +369,34 @@ export class SortableTable { const links = this.exportTableLinksToCSV(); this.downloadCSV(links, 'classyLinks.csv', ' Download Links as CSV'); } + + /** + * Compute the visible height of the table. This is needed for display: sticky + * to work. But adds a bit of complication because if the window is resized + * the values also need to be recomputed. + */ + public updateTableHeight() { + Log.info("SortableTable::updateTableHeight() - table: " + this.divName); + + if (this.numRows() < 20) { + // if the number of rows is low, don't bother doing this + Log.info("SortableTable::updateTableHeight() - skipped; # rows: " + + this.numRows() + "; table: " + this.divName); + return; + } + + try { + let offset = 0; + let node: any = document.querySelector(this.divName); + while (node.offsetParent && node.offsetParent.id !== "wrapper") { + offset += node.offsetTop; + node = node.offsetParent; + } + const visibleHeight = (node.offsetHeight - offset) + "px"; + node = document.querySelector(this.divName); + node.style.height = visibleHeight; + } catch (err) { + Log.error("SortableTable::updateTableHeight() - ERROR: " + err.messsage); + } + } } From 85ca1e23c8670763dc1de70d3e2db9029296e67f Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 10:48:42 -0700 Subject: [PATCH 037/104] test string changes --- packages/autotest/test/GitHubAutoTestSpec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/autotest/test/GitHubAutoTestSpec.ts b/packages/autotest/test/GitHubAutoTestSpec.ts index 12802abbb..362dd12f1 100644 --- a/packages/autotest/test/GitHubAutoTestSpec.ts +++ b/packages/autotest/test/GitHubAutoTestSpec.ts @@ -451,7 +451,7 @@ describe("GitHubAutoTest", () => { allData = await data.getAllData(); expect(gitHubMessages.length).to.equal(1); // should generate a warning - expect(gitHubMessages[0].message).to.equal("This commit has been queued for processing against d1. Your results will be posted here as soon as they are ready."); + expect(gitHubMessages[0].message).to.contain("queued for processing against d1"); expect(allData.comments.length).to.equal(1); // comment event should not have been saved }); @@ -474,7 +474,7 @@ describe("GitHubAutoTest", () => { await at.handleCommentEvent(TestData.commentRecordUserA); allData = await data.getAllData(); expect(gitHubMessages.length).to.equal(1); // should generate a warning - expect(gitHubMessages[0].message).to.equal("This commit is still queued for processing against d1. Your results will be posted here as soon as they are ready."); + expect(gitHubMessages[0].message).to.contain("queued for processing against d1"); expect(allData.comments.length).to.equal(1); await Util.timeout(WAIT); // just clear the buffer before moving onto the next test @@ -501,7 +501,7 @@ describe("GitHubAutoTest", () => { allData = await data.getAllData(); Log.test("1: - ghMessages: " + JSON.stringify(gitHubMessages)); expect(gitHubMessages.length).to.equal(1); // should generate a warning - expect(gitHubMessages[0].message).to.equal("This commit is still queued for processing against d1. Your results will be posted here as soon as they are ready."); + expect(gitHubMessages[0].message).to.contain("queued for processing against d1"); Log.test("1: - allData: " + JSON.stringify(allData)); expect(allData.comments.length).to.equal(1); expect(allData.feedback.length).to.equal(0); // don't charge for feedback until it is given From d8f95fca477c86f581c982d5f7488e545e2e92c6 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 10:53:38 -0700 Subject: [PATCH 038/104] improved auth logging --- .../portal/backend/src/server/common/AuthRoutes.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/portal/backend/src/server/common/AuthRoutes.ts b/packages/portal/backend/src/server/common/AuthRoutes.ts index fc8314dfe..7f2d662e8 100644 --- a/packages/portal/backend/src/server/common/AuthRoutes.ts +++ b/packages/portal/backend/src/server/common/AuthRoutes.ts @@ -154,16 +154,16 @@ export class AuthRoutes implements IREST { public static async performGetCredentials(user: string, token: string): Promise<{ isAdmin: boolean, isStaff: boolean }> { const isValid = await AuthRoutes.ac.isValid(user, token); - Log.trace('AuthRoutes::getCredentials(..) - in isValid(..)'); + Log.trace("AuthRoutes::getCredentials( " + user + " ) - in isValid(..)"); if (isValid === false) { - Log.error('AuthRoutes::getCredentials(..) - isValid false'); - throw new Error("Login error; user not valid."); + Log.error("AuthRoutes::getCredentials( " + user + " ) - isValid false"); + throw new Error("Login error; user: " + user + " not valid."); } - Log.trace('AuthRoutes::getCredentials(..) - isValid true'); + Log.trace("AuthRoutes::getCredentials( " + user + " ) - isValid true"); let isPrivileged = await AuthRoutes.ac.isPrivileged(user, token); - if (typeof isPrivileged === 'undefined' || isPrivileged === null) { - Log.warn('AuthRoutes::getCredentials(..) - failsafe; DEBUG this case?'); + if (typeof isPrivileged === "undefined" || isPrivileged === null) { + Log.warn("AuthRoutes::getCredentials( " + user + " ) - failsafe; DEBUG this case?"); isPrivileged = {isAdmin: false, isStaff: false}; // fail safe } return {isAdmin: isPrivileged.isAdmin, isStaff: isPrivileged.isStaff}; From 61cb30742be336991e416ed3cfc333ee0387c264 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 11:02:13 -0700 Subject: [PATCH 039/104] fix #363 (and add new tests for feature) --- .../autotest/src/github/GitHubAutoTest.ts | 10 ++-- packages/autotest/test/GitHubAutoTestSpec.ts | 51 ++++++++++++++++++- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index be739f9e1..f9e768027 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -66,8 +66,7 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { if (typeof org !== "undefined" && typeof info.orgId !== "undefined" && org !== info.orgId) { Log.warn("GitHubAutoTest::handlePushEvent(..) - ignored, org: " + info.orgId + " does not match current course: " + org); - // TODO: turn statement on to fix #363 - // return false; + return false; } Log.info("GitHubAutoTest::handlePushEvent(..) - start; commit: " + info.commitSHA); @@ -216,10 +215,9 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { " does not match current course: " + org); // no deliverable, give warning and abort - // TODO: turn statements below on to fix #363 - // const msg = "This commit appears to be from a prior version of the course; AutoTest cancelled."; - // await this.postToGitHub(info, {url: info.postbackURL, message: msg}); - // return false; + const msg = "This commit appears to be from a prior version of the course; AutoTest request cancelled."; + await this.postToGitHub(info, {url: info.postbackURL, message: msg}); + return false; } // TODO: invalid personId diff --git a/packages/autotest/test/GitHubAutoTestSpec.ts b/packages/autotest/test/GitHubAutoTestSpec.ts index 362dd12f1..a5c802a68 100644 --- a/packages/autotest/test/GitHubAutoTestSpec.ts +++ b/packages/autotest/test/GitHubAutoTestSpec.ts @@ -19,7 +19,7 @@ import "./GlobalSpec"; // load first import {TestData} from "./TestData"; /* tslint:disable:max-line-length */ -describe("GitHubAutoTest", () => { +describe.only("GitHubAutoTest", () => { Config.getInstance(); @@ -237,6 +237,13 @@ describe("GitHubAutoTest", () => { expect(meetsPreconditions).to.be.false; info.delivId = 'd1'; + Log.test('wrong term'); + const oldOrg = info.orgId; + info.orgId = 'INVALIDTERM'; + meetsPreconditions = await at["checkCommentPreconditions"](info); + expect(meetsPreconditions).to.be.false; + info.orgId = oldOrg; + Log.test('invalid delivId'); info.delivId = 'd_' + Date.now(); meetsPreconditions = await at["checkCommentPreconditions"](info); @@ -626,6 +633,48 @@ describe("GitHubAutoTest", () => { expect(allData.feedback.length).to.equal(0); // no charge }).timeout(WAIT * 10); + it("Should ignore a request for a push from a prior version of the course.", async () => { + expect(at).not.to.equal(null); + + // start fresh + await data.clearData(); + // gh.messages = []; + stubDependencies(); + + let allData = await data.getAllData(); + expect(gitHubMessages.length).to.equal(0); + expect(allData.comments.length).to.equal(0); + expect(allData.pushes.length).to.equal(0); + + // SETUP: add a push with no output records + const push = Object.assign({}, TestData.pushEventPostback); + push.orgId = "SOMERANDOMTERM"; + await at.handlePushEvent(push); + + allData = await data.getAllData(); + expect(gitHubMessages.length).to.equal(0); // should not be any feedback yet + expect(allData.comments.length).to.equal(0); + expect(allData.pushes.length).to.equal(0); // push should not save as it was dropped + + // + // + // // TEST: send a comment (this is the previous test) + // // await at.handleCommentEvent(commentRecordUserA); + // // allData = await data.getAllData(); + // // expect(gh.messages.length).to.equal(1); // should generate a warning + // // expect(gh.messages[0].message).to.equal("This commit is still queued for processing against d1. Your results will be posted here as soon as they are ready."); + // // expect(allData.comments.length).to.equal(1); + // // expect(allData.feedback.length).to.equal(0); // don't charge for feedback until it is given + // + // // Wait for it! + // await Util.timeout(WAIT); + // allData = await data.getAllData(); + // expect(gitHubMessages.length).to.equal(1); // should post response + // expect(gitHubMessages[0].message).to.equal("Build Problem Encountered."); + // expect(allData.comments.length).to.equal(0); + // expect(allData.feedback.length).to.equal(0); // no charge + }).timeout(WAIT * 10); + it("Should give a user the results message on a commit that has been finished.", async () => { expect(at).not.to.equal(null); From e37a81ba2da35dd52d1012f5f7ffba9bf8dbd46d Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 11:06:09 -0700 Subject: [PATCH 040/104] more robust org parsing --- packages/autotest/src/github/GitHubUtil.ts | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index b297950d2..353d44fc2 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -102,11 +102,15 @@ export class GitHubUtil { // NEXT: need cloneURL const cloneURL = String(payload.repository.clone_url); - const orgId = payload.repository.full_name.substr(0, - payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); - Log.info("GitHubUtil::processComment(..) - full_name: " + payload.repository.full_name + - "; name: " + payload.repository.name + "; org: " + orgId); - + let orgId; + try { + orgId = payload.repository.full_name.substr(0, + payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); + Log.info("GitHubUtil::processComment(..) - full_name: " + payload.repository.full_name + + "; name: " + payload.repository.name + "; org: " + orgId); + } catch (err) { + Log.warn("GitHubUtil::processComment(..) - failed to parse org: " + err); + } const requester = String(payload.comment.user.login); // .toLowerCase(); const message = payload.comment.body; @@ -207,10 +211,15 @@ export class GitHubUtil { const cloneURL = payload.repository.clone_url; const ref = payload.ref; const pusher = await new ClassPortal().getPersonId(payload.pusher.name); - const org = payload.repository.full_name.substr(0, - payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); - Log.info("GitHubUtil::processPush(..) - full_name: " + payload.repository.full_name + - "; name: " + payload.repository.name + "; org: " + org); + let org; + try { + org = payload.repository.full_name.substr(0, + payload.repository.full_name.lastIndexOf(payload.repository.name) - 1); + Log.info("GitHubUtil::processPush(..) - full_name: " + payload.repository.full_name + + "; name: " + payload.repository.name + "; org: " + org); + } catch (err) { + Log.warn("GitHubUtil::processPush(..) - failed to parse org: " + err); + } Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; projectURL: " + projectURL + "; ref: " + ref); From 8ea9a1030a4ce168017d33a63657a1904eb62171 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 14:23:03 -0700 Subject: [PATCH 041/104] more pagination logging for prod --- packages/autotest/src/autotest/ClassPortal.ts | 7 +++--- packages/autotest/src/autotest/Queue.ts | 2 +- .../backend/src/controllers/GitHubActions.ts | 22 +++++++++---------- .../src/controllers/ResultsController.ts | 5 +++-- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/autotest/src/autotest/ClassPortal.ts b/packages/autotest/src/autotest/ClassPortal.ts index 53c0f9c3b..9ddfcb166 100644 --- a/packages/autotest/src/autotest/ClassPortal.ts +++ b/packages/autotest/src/autotest/ClassPortal.ts @@ -270,7 +270,7 @@ export class ClassPortal implements IClassPortal { public async formatFeedback(res: AutoTestResultTransport): Promise { const start = Date.now(); - Log.info("ClassPortal::formatFeedback(..) - start; delivId: " + + Log.trace("ClassPortal::formatFeedback(..) - start; delivId: " + res.delivId + "; URL: " + res.commitURL); let feedback: string = ''; @@ -302,9 +302,10 @@ export class ClassPortal implements IClassPortal { } } - Log.info("ClassPortal::formatFeedback(..) - feedback generated; URL: " + + Log.trace("ClassPortal::formatFeedback(..) - feedback generated; URL: " + res.commitURL + "; feedback: " + msg + "; took: " + Util.took(start)); - + Log.info("ClassPortal::formatFeedback(..) - feedback generated; deliv: " + + res.delivId + "; repo: " + res.repoId + "; feedback: " + msg + "; took: " + Util.took(start)); return feedback; } diff --git a/packages/autotest/src/autotest/Queue.ts b/packages/autotest/src/autotest/Queue.ts index fe5cf7349..a498aeeb2 100644 --- a/packages/autotest/src/autotest/Queue.ts +++ b/packages/autotest/src/autotest/Queue.ts @@ -234,7 +234,7 @@ export class Queue { this.slots.push(input); Log.info("Queue::scheduleNext() - " + this.getName() + " done; delivId: " + - input.delivId + "; commitURL: " + input.target.commitURL); + input.delivId + "; repo: " + input.target.repoId); return input; } diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index 9f30c1eb0..e7a98275b 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -562,7 +562,7 @@ export class GitHubActions implements IGitHubActions { } private async handlePagination(uri: string, options: RequestInit): Promise { - Log.trace("GitHubActions::handlePagination(..) - start; PAGE_SIZE: " + this.pageSize); + Log.info("GitHubActions::handlePagination(..) - start; PAGE_SIZE: " + this.pageSize); const start = Date.now(); try { @@ -579,15 +579,15 @@ export class GitHubActions implements IGitHubActions { let lastPage: number = -1; const linkText = response.headers.get('link'); - Log.trace('GitHubActions::handlePagination(..) - linkText: ' + linkText); + Log.info('GitHubActions::handlePagination(..) - linkText: ' + linkText); const linkParts = linkText.split(','); for (const p of linkParts) { const pparts = p.split(';'); if (pparts[1].indexOf('last')) { const pText = pparts[0].split('&page=')[1]; - Log.trace('GitHubActions::handlePagination(..) - last page pText:_' + pText + '_; p: ' + p); + Log.info('GitHubActions::handlePagination(..) - last page pText:_' + pText + '_; p: ' + p); lastPage = Number(pText.match(/\d+/)[0]); - Log.trace('GitHubActions::handlePagination(..) - last page: ' + lastPage); + Log.info('GitHubActions::handlePagination(..) - last page: ' + lastPage); } } @@ -596,30 +596,30 @@ export class GitHubActions implements IGitHubActions { const pparts = p.split(';'); if (pparts[1].indexOf('next')) { let pText = pparts[0].split('&page=')[0].trim(); - Log.trace('GitHubActions::handlePagination(..) - pt: ' + pText); + Log.info('GitHubActions::handlePagination(..) - pt: ' + pText); pText = pText.substring(1); pText = pText + "&page="; pageBase = pText; - Log.trace('GitHubActions::handlePagination(..) - page base: ' + pageBase); + Log.info('GitHubActions::handlePagination(..) - page base: ' + pageBase); } } - Log.trace("GitHubActions::handlePagination(..) - handling pagination; # pages: " + lastPage); + Log.info("GitHubActions::handlePagination(..) - handling pagination; # pages: " + lastPage); for (let i = 2; i <= lastPage; i++) { const pageUri = pageBase + i; - Log.trace('GitHubActions::handlePagination(..) - page to request: ' + pageUri); + Log.info('GitHubActions::handlePagination(..) - page to request: ' + pageUri); uri = pageUri; // not sure why this is needed // NOTE: this needs to be slowed down to prevent DNS problems (issuing 10+ concurrent dns requests can be problematic) await Util.delay(100); paginationPromises.push(fetch(uri, options as any)); } } else { - Log.trace("GitHubActions::handlePagination(..) - single page"); + Log.info("GitHubActions::handlePagination(..) - single page"); raw = body; // don't put anything on the paginationPromise if it isn't paginated } - Log.trace("GitHubActions::handlePagination(..) - requesting all"); + Log.info("GitHubActions::handlePagination(..) - requesting all"); // this block won't do anything if we just did the raw thing above (aka no pagination) const responses: any[] = await Promise.all(paginationPromises); // Log.trace("GitHubActions::handlePagination(..) - requests complete"); @@ -627,7 +627,7 @@ export class GitHubActions implements IGitHubActions { for (const res of responses) { raw = raw.concat(await res.json()); } - Log.trace("GitHubActions::handlePagination(..) - total count: " + raw.length + "; took: " + Util.took(start)); + Log.info("GitHubActions::handlePagination(..) - total count: " + raw.length + "; took: " + Util.took(start)); return raw; } catch (err) { diff --git a/packages/portal/backend/src/controllers/ResultsController.ts b/packages/portal/backend/src/controllers/ResultsController.ts index 9f41c420b..df52c8380 100644 --- a/packages/portal/backend/src/controllers/ResultsController.ts +++ b/packages/portal/backend/src/controllers/ResultsController.ts @@ -18,7 +18,7 @@ export class ResultsController { const results = await this.db.getAllResults(); // NOTE: this block can go away once all results have been migrated to use target instead of pushInfo - results.sort(function(a: Result, b: Result) { + results.sort(function (a: Result, b: Result) { let tsA = 0; let tsB = 0; if (typeof a.input.target !== 'undefined') { @@ -57,7 +57,8 @@ export class ResultsController { } public async createResult(record: AutoTestResult): Promise { - Log.info("ResultsController::createResult(..) - start for commit: " + record.commitURL); + Log.info("ResultsController::createResult(..) - start; deliv: " + record.delivId + "; repo: " + + record.repoId + "; SHA: " + record.commitSHA); Log.trace("GradesController::createResult(..) - payload: " + JSON.stringify(record)); const start = Date.now(); From 2b71706dcc7b18a34d6cc51932e208359bf1fc40 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 14:44:14 -0700 Subject: [PATCH 042/104] logging --- packages/autotest/src/autotest/ClassPortal.ts | 4 +- packages/autotest/src/github/GitHubUtil.ts | 4 +- packages/autotest/src/server/RouteHandler.ts | 4 +- .../src/controllers/AdminController.ts | 86 ++++++------ .../src/controllers/DatabaseController.ts | 123 +++++++++--------- .../backend/src/controllers/GitHubActions.ts | 4 +- 6 files changed, 117 insertions(+), 108 deletions(-) diff --git a/packages/autotest/src/autotest/ClassPortal.ts b/packages/autotest/src/autotest/ClassPortal.ts index 9ddfcb166..d50ec776c 100644 --- a/packages/autotest/src/autotest/ClassPortal.ts +++ b/packages/autotest/src/autotest/ClassPortal.ts @@ -157,7 +157,7 @@ export class ClassPortal implements IClassPortal { }; const res = await fetch(url, opts); - Log.info("ClassPortal::personId( " + githubId + " ) - success; payload: " + res + "; took: " + Util.took(start)); + Log.trace("ClassPortal::personId( " + githubId + " ) - success; payload: " + res + "; took: " + Util.took(start)); const json: Payload = await res.json() as Payload; if (typeof json.success !== 'undefined') { return json.success; // AutoTestPersonIdTransport @@ -254,7 +254,7 @@ export class ClassPortal implements IClassPortal { const json = await res.json(); if (typeof json.success !== 'undefined') { Log.info("ClassPortal::sendGrade(..) - grade accepted; delivId: " + grade.delivId + - "; url: " + grade.URL + "; took: " + Util.took(start)); + "; repo: " + grade.repoId + "; took: " + Util.took(start)); return json; } else { Log.error("ClassPortal::sendGrade(..) - ERROR; grade not accepted: " + JSON.stringify(json)); diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index 353d44fc2..c42678cd9 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -221,7 +221,7 @@ export class GitHubUtil { Log.warn("GitHubUtil::processPush(..) - failed to parse org: " + err); } - Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; projectURL: " + projectURL + "; ref: " + ref); + Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; ref: " + ref); if (payload.deleted === true && payload.head_commit === null) { // commit deleted a branch, do nothing @@ -243,7 +243,7 @@ export class GitHubUtil { Log.info("GitHubUtil::processPush(..) - branch added; URL: " + commitURL); } - Log.info("GitHubUtil::processPush(..) - sha: " + commitSHA + "; commitURL: " + commitURL); + Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; sha: " + commitSHA); const postbackURL = payload.repository.commits_url.replace("{/sha}", "/" + commitSHA) + "/comments"; // this gives the timestamp of the last commit (which could be forged), not the time of the push diff --git a/packages/autotest/src/server/RouteHandler.ts b/packages/autotest/src/server/RouteHandler.ts index 4fa8be928..959b1cdc8 100644 --- a/packages/autotest/src/server/RouteHandler.ts +++ b/packages/autotest/src/server/RouteHandler.ts @@ -66,7 +66,7 @@ export default class RouteHandler { githubSecret = null; } - Log.info("RouteHandler::postGithubHook(..) - start; handling event: " + githubEvent + "; signature: " + githubSecret); + Log.info("RouteHandler::postGithubHook(..) - start; handling event: " + githubEvent); // + "; signature: " + githubSecret); const body = req.body; const handleError = function(msg: string) { @@ -125,6 +125,8 @@ export default class RouteHandler { } else { handleError("Invalid payload signature."); } + + Log.info("RouteHandler::postGithubHook(..) - done handling event: " + githubEvent); return next(); } diff --git a/packages/portal/backend/src/controllers/AdminController.ts b/packages/portal/backend/src/controllers/AdminController.ts index fbb376e18..55c64e966 100644 --- a/packages/portal/backend/src/controllers/AdminController.ts +++ b/packages/portal/backend/src/controllers/AdminController.ts @@ -88,14 +88,14 @@ export class AdminController { for (const personId of peopleIds) { const newGrade: Grade = { - personId: personId, - delivId: grade.delivId, - score: grade.score, - comment: grade.comment, - urlName: grade.urlName, - URL: grade.URL, + personId: personId, + delivId: grade.delivId, + score: grade.score, + comment: grade.comment, + urlName: grade.urlName, + URL: grade.URL, timestamp: grade.timestamp, - custom: grade.custom + custom: grade.custom }; Log.trace("AdminController::processNewAutoTestGrade( .. ) - getting grade for " + personId); // NOTE: for hangup debugging @@ -108,7 +108,7 @@ export class AdminController { if (shouldSave === true) { Log.info("AdminController::processNewAutoTestGrade( .. ) - saving grade for delivId: " - + newGrade.delivId + "; URL: " + grade.URL); + + newGrade.delivId + "; repo: " + grade.repoId); await this.dbc.writeAudit(AuditLabel.GRADE_AUTOTEST, 'AutoTest', existingGrade, newGrade, {repoId: grade.repoId}); await this.gc.saveGrade(newGrade); @@ -127,9 +127,9 @@ export class AdminController { if (record === null) { // create default and write it record = { - id: Config.getInstance().getProp(ConfigKey.name), + id: Config.getInstance().getProp(ConfigKey.name), defaultDeliverableId: null, - custom: {} + custom: {} }; await this.dbc.writeCourseRecord(record); } @@ -159,13 +159,13 @@ export class AdminController { for (const person of people) { if (person.kind === PersonKind.STUDENT || person.kind === null) { // null should be set on first login const studentTransport = { - id: person.id, - firstName: person.fName, - lastName: person.lName, - githubId: person.githubId, - userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + person.githubId, + id: person.id, + firstName: person.fName, + lastName: person.lName, + githubId: person.githubId, + userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + person.githubId, studentNum: person.studentNumber, - labId: person.labId + labId: person.labId }; students.push(studentTransport); } @@ -183,10 +183,10 @@ export class AdminController { const teams: TeamTransport[] = []; for (const team of allTeams) { const teamTransport: TeamTransport = { - id: team.id, + id: team.id, delivId: team.delivId, - people: team.personIds, - URL: team.URL + people: team.personIds, + URL: team.URL // repoName: team.repoName, // repoUrl: team.repoUrl }; @@ -206,8 +206,8 @@ export class AdminController { const repos: RepositoryTransport[] = []; for (const repo of allRepos) { const repoTransport: RepositoryTransport = { - id: repo.id, - URL: repo.URL, + id: repo.id, + URL: repo.URL, delivId: repo.delivId }; repos.push(repoTransport); @@ -230,15 +230,15 @@ export class AdminController { for (const grade of allGrades) { const p = await pc.getPerson(grade.personId); // TODO: slow action for just githubid const gradeTrans: GradeTransport = { - personId: grade.personId, + personId: grade.personId, personURL: Config.getInstance().getProp(ConfigKey.githubHost) + '/' + p.githubId, - delivId: grade.delivId, - score: grade.score, - comment: grade.comment, - urlName: grade.urlName, - URL: grade.URL, + delivId: grade.delivId, + score: grade.score, + comment: grade.comment, + urlName: grade.urlName, + URL: grade.URL, timestamp: grade.timestamp, - custom: grade.custom + custom: grade.custom }; grades.push(gradeTrans); } @@ -270,7 +270,7 @@ export class AdminController { for (const result of allResults) { const repoId = result.input.target.repoId; if (results.length < NUM_RESULTS) { - const resultTrans = await this.createDashboardTransport(result); + const resultTrans = await this.createDashboardTransport(result); // just return the first result for a repo, unless they are specified if (reqRepoId !== 'any' || repoIds.indexOf(repoId) < 0) { results.push(resultTrans); @@ -310,11 +310,11 @@ export class AdminController { return { ...resultSummary, - testPass: testPass, - testFail: testFail, + testPass: testPass, + testFail: testFail, testError: testError, - testSkip: testSkip, - custom: {}, + testSkip: testSkip, + custom: {}, }; } @@ -444,7 +444,7 @@ export class AdminController { const state = this.selectState(result); - return { + return { repoId: repoId, repoURL: repoURL, delivId: result.delivId, @@ -494,7 +494,7 @@ export class AdminController { delivs.push(delivTransport); } - delivs = delivs.sort(function(d1: DeliverableTransport, d2: DeliverableTransport) { + delivs = delivs.sort(function (d1: DeliverableTransport, d2: DeliverableTransport) { return d1.id.localeCompare(d2.id); }); @@ -695,7 +695,7 @@ export class AdminController { // remove any people who are already on teams for (const team of delivTeams) { for (const personId of team.personIds) { - const index = allPeople.map(function(p: Person) { + const index = allPeople.map(function (p: Person) { return p.id; }).indexOf(personId); if (index >= 0) { @@ -807,22 +807,22 @@ export class AdminController { if (repo.URL === null) { // key check: repo.URL is only set if the repo has been provisioned const futureTeams: Array> = repo.teamIds.map((teamId) => this.dbc.getTeam(teamId)); const teams: Team[] = await Promise.all(futureTeams); - Log.info("AdminController::performProvision( .. ) - about to provision: " + repo.id); + Log.trace("AdminController::performProvision( .. ) - about to provision: " + repo.id); let success = await ghc.provisionRepository(repo.id, teams, importURL); success = success && await cc.finalizeProvisionedRepo(repo, teams); - Log.info("AdminController::performProvision( .. ) - provisioned: " + repo.id + "; success: " + success); + Log.trace("AdminController::performProvision( .. ) - provisioned: " + repo.id + "; success: " + success); if (success === true) { repo.URL = config.getProp(ConfigKey.githubHost) + "/" + config.getProp(ConfigKey.org) + "/" + repo.id; repo.custom.githubCreated = true; // might not be necessary anymore; should just use repo.URL !== null await dbc.writeRepository(repo); - Log.info("AdminController::performProvision( .. ) - success: " + repo.id + "; URL: " + repo.URL); + Log.trace("AdminController::performProvision( .. ) - success: " + repo.id + "; URL: " + repo.URL); provisionedRepos.push(repo); } else { Log.warn("AdminController::performProvision( .. ) - provision FAILED: " + repo.id + "; URL: " + repo.URL); } - Log.info("AdminController::performProvision( .. ) - done provisioning: " + repo.id + "; forced wait"); + Log.trace("AdminController::performProvision( .. ) - done provisioning: " + repo.id + "; forced wait"); await Util.delay(2 * 1000); // after any provisioning wait a bit // Log.info("AdminController::performProvision( .. ) - done for repo: " + repo.id + "; wait complete"); Log.info("AdminController::performProvision( .. ) ***** DONE *****; repo: " + @@ -857,7 +857,7 @@ export class AdminController { const cc = await Factory.getCourseController(this.gh); const allTeams: Team[] = await this.tc.getAllTeams(); - Log.info("AdminController::planRelease( " + deliv.id + " ) - # teams: " + allTeams.length); + Log.trace("AdminController::planRelease( " + deliv.id + " ) - # teams: " + allTeams.length); const delivTeams: Team[] = []; for (const team of allTeams) { @@ -951,8 +951,8 @@ export class AdminController { await Util.delay(200); // after any releasing wait a short bit } else { - Log.info("AdminController::performRelease( .. ) - skipped; repo not yet provisioned: " + - repo.id + "; URL: " + repo.URL); + Log.info("AdminController::performRelease( .. ) - skipped; repo not yet provisioned: " + + repo.id); // + "; URL: " + repo.URL); } } catch (err) { Log.error("AdminController::performRelease( .. ) - FAILED: " + diff --git a/packages/portal/backend/src/controllers/DatabaseController.ts b/packages/portal/backend/src/controllers/DatabaseController.ts index f990fd2f3..114601fe8 100644 --- a/packages/portal/backend/src/controllers/DatabaseController.ts +++ b/packages/portal/backend/src/controllers/DatabaseController.ts @@ -117,17 +117,18 @@ export class DatabaseController { const start = Date.now(); Log.trace("DatabaseController::getGradedResults() - start"); const pipeline = [ - { $match : { delivId : deliv } }, - { $group: { _id: '$URL' } }, - { $lookup: - { - from: this.RESULTCOLL, - localField: '_id', - foreignField: 'commitURL', - as: 'results' - } + {$match: {delivId: deliv}}, + {$group: {_id: '$URL'}}, + { + $lookup: + { + from: this.RESULTCOLL, + localField: '_id', + foreignField: 'commitURL', + as: 'results' + } }, - { $project: { result: { $arrayElemAt: ["$results", 0] } } } + {$project: {result: {$arrayElemAt: ["$results", 0]}}} ]; const collection = await this.getCollection(this.GRADECOLL, QueryKind.SLOW); const results = (await collection.aggregate(pipeline).toArray()).map((r) => r.result); @@ -140,28 +141,32 @@ export class DatabaseController { const start = Date.now(); Log.trace("DatabaseController::getBestResults() - start"); const pipeline = [ - { $match : { delivId : deliv } }, - { $group: { _id: '$repoId', maxScore: { $max: "$output.report.scoreOverall" } } }, - { $lookup: - { - from: this.RESULTCOLL, - let: { srcRepo: "$_id", score: "$maxScore" }, - pipeline: [ - { $match: - { $expr: - { $and: - [ - { $eq: [ "$repoId", "$$srcRepo" ] }, - { $eq: [ "$output.report.scoreOverall", "$$score" ] } - ] - } + {$match: {delivId: deliv}}, + {$group: {_id: '$repoId', maxScore: {$max: "$output.report.scoreOverall"}}}, + { + $lookup: + { + from: this.RESULTCOLL, + let: {srcRepo: "$_id", score: "$maxScore"}, + pipeline: [ + { + $match: + { + $expr: + { + $and: + [ + {$eq: ["$repoId", "$$srcRepo"]}, + {$eq: ["$output.report.scoreOverall", "$$score"]} + ] + } + } } - } - ], - as: 'results' - } + ], + as: 'results' + } }, - { $project: { result: { $arrayElemAt: ["$results", 0] } } } + {$project: {result: {$arrayElemAt: ["$results", 0]}}} ]; const collection = await this.getCollection(this.RESULTCOLL, QueryKind.SLOW); const results = (await collection.aggregate(pipeline).toArray()).map((r) => r.result); @@ -171,7 +176,7 @@ export class DatabaseController { } public async getTeamsForPerson(personId: string): Promise { - Log.info("DatabaseController::getTeamsForPerson() - start"); + Log.trace("DatabaseController::getTeamsForPerson( " + personId + " ) - start"); const teams = await this.readRecords(this.TEAMCOLL, QueryKind.FAST, false, {}); const myTeams = []; for (const t of teams as Team[]) { @@ -188,18 +193,18 @@ export class DatabaseController { const query = [ { $lookup: { - from: "teams", - localField: "teamIds", + from: "teams", + localField: "teamIds", foreignField: "id", - as: "teams" + as: "teams" } }, { $lookup: { - from: "people", - localField: "teams.personIds", + from: "people", + localField: "teams.personIds", foreignField: "id", - as: "teammembers" + as: "teammembers" } }, { @@ -435,12 +440,12 @@ export class DatabaseController { finalLabel = finalLabel + 'UPDATE'; } const auditRecord: AuditEvent = { - label: finalLabel, + label: finalLabel, timestamp: Date.now(), - personId: personId, - before: before, - after: after, - custom: custom + personId: personId, + before: before, + after: after, + custom: custom }; return await this.writeRecord(this.AUDITCOLL, auditRecord); } catch (err) { @@ -677,7 +682,9 @@ export class DatabaseController { } // _ are to help diagnose whitespace in dbname/mongoUrl - Log.info("DatabaseController::open() - db null; making new connection to: _" + dbName + "_ on: _" + dbHost + "_"); + // too much info, logs password + // Log.info("DatabaseController::open() - db null; making new connection to: _" + dbName + "_ on: _" + dbHost + "_"); + Log.info("DatabaseController::open() - db null; making new connection to: _" + dbName + "_"); const client = await MongoClient.connect(dbHost); if (kind === 'slow') { @@ -735,14 +742,14 @@ export class DatabaseController { let team = await this.getTeam(teamName); if (team === null) { const newTeam: Team = { - id: teamName, - delivId: null, // null for special teams - githubId: null, // to be filled in later - URL: null, // to be filled in later + id: teamName, + delivId: null, // null for special teams + githubId: null, // to be filled in later + URL: null, // to be filled in later personIds: [], // empty for special teams // repoName: null, // null for special teams // repoUrl: null, - custom: {} + custom: {} }; await this.writeTeam(newTeam); } @@ -750,14 +757,14 @@ export class DatabaseController { team = await this.getTeam(teamName); if (team === null) { const newTeam: Team = { - id: teamName, - delivId: null, // null for special teams - githubId: null, // to be filled in later - URL: null, // to be filled in later + id: teamName, + delivId: null, // null for special teams + githubId: null, // to be filled in later + URL: null, // to be filled in later personIds: [], // empty for special teams // repoName: null, // null for special teams // repoUrl: null, - custom: {} + custom: {} }; await this.writeTeam(newTeam); } @@ -765,14 +772,14 @@ export class DatabaseController { team = await this.getTeam(teamName); if (team === null) { const newTeam: Team = { - id: teamName, - delivId: null, // null for special teams - githubId: null, // to be filled in later - URL: null, // to be filled in later + id: teamName, + delivId: null, // null for special teams + githubId: null, // to be filled in later + URL: null, // to be filled in later personIds: [], // empty for special teams // repoName: null, // null for special teams // repoUrl: null, - custom: {} + custom: {} }; await this.writeTeam(newTeam); } diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index e7a98275b..4626cf874 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -343,12 +343,12 @@ export class GitHubActions implements IGitHubActions { Log.info("GitHubAction::createRepo( " + repoName + " ) - request complete"); const url = body.html_url; - Log.info("GitHubAction::createRepo( " + repoName + " ) - db start"); + Log.trace("GitHubAction::createRepo( " + repoName + " ) - db start"); const repo = await this.dc.getRepository(repoName); repo.URL = url; // only update this field in the existing Repository record repo.cloneURL = body.clone_url; // only update this field in the existing Repository record await this.dc.writeRepository(repo); - Log.info("GitHubAction::createRepo( " + repoName + " ) - db done"); + Log.trace("GitHubAction::createRepo( " + repoName + " ) - db done"); Log.info("GitHubAction::createRepo(..) - success; URL: " + url + "; delaying to prep repo. Took: " + Util.took(start)); await Util.delay(this.PAUSE); From e38b4a68a34175ad0e908cb398947ab538d89d61 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 14:56:58 -0700 Subject: [PATCH 043/104] add logging to all callers of handlepagination to debug withdraw problem --- packages/autotest/src/github/GitHubUtil.ts | 4 ++-- packages/portal/backend/src/controllers/CourseController.ts | 2 +- packages/portal/backend/src/controllers/GitHubActions.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/autotest/src/github/GitHubUtil.ts b/packages/autotest/src/github/GitHubUtil.ts index c42678cd9..f45c85e79 100644 --- a/packages/autotest/src/github/GitHubUtil.ts +++ b/packages/autotest/src/github/GitHubUtil.ts @@ -235,12 +235,12 @@ export class GitHubUtil { if (typeof payload.commits !== "undefined" && payload.commits.length > 0) { commitSHA = payload.commits[payload.commits.length - 1].id; commitURL = payload.commits[payload.commits.length - 1].url; - Log.info("GitHubUtil::processPush(..) - regular push; # commits: " + payload.commits.length + "; URL: " + commitURL); + Log.info("GitHubUtil::processPush(..) - regular push; repo: " + repo + "; # commits: " + payload.commits.length); } else { // use this one when creating a new branch (may not have any commits) commitSHA = payload.head_commit.id; commitURL = payload.head_commit.url; - Log.info("GitHubUtil::processPush(..) - branch added; URL: " + commitURL); + Log.info("GitHubUtil::processPush(..) - branch added; repo: " + repo); } Log.info("GitHubUtil::processPush(..) - repo: " + repo + "; sha: " + commitSHA); diff --git a/packages/portal/backend/src/controllers/CourseController.ts b/packages/portal/backend/src/controllers/CourseController.ts index cf52dcc95..46c2f7409 100644 --- a/packages/portal/backend/src/controllers/CourseController.ts +++ b/packages/portal/backend/src/controllers/CourseController.ts @@ -118,7 +118,7 @@ export class CourseController implements ICourseController { */ public handleNewAutoTestGrade(deliv: Deliverable, newGrade: Grade, existingGrade: Grade): Promise { const LOGPRE = "CourseController::handleNewAutoTestGrade( " + deliv.id + ", " + - newGrade.personId + ", " + newGrade.score + ", ... ) - URL: " + newGrade.URL + " - "; + newGrade.personId + ", " + newGrade.score + ", ... ) - start - "; Log.trace(LOGPRE + "start"); diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index 4626cf874..119fb417e 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -1008,7 +1008,7 @@ export class GitHubActions implements IGitHubActions { * @returns {Promise} */ public async getTeamMembers(teamName: string): Promise { - Log.trace("GitHubAction::getTeamMembers( " + teamName + " ) - start"); + Log.info("GitHubAction::getTeamMembers( " + teamName + " ) - start"); if (teamName === null) { throw new Error("GitHubAction::getTeamMembers( null ) - null team requested"); From 88a3be97992a8a204635906524aa0bac37e1e0ab Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 17:16:37 -0700 Subject: [PATCH 044/104] pagination logging --- packages/autotest/src/autotest/AutoTest.ts | 2 +- .../autotest/src/github/GitHubAutoTest.ts | 4 +- .../src/controllers/AdminController.ts | 2 +- .../backend/src/controllers/GitHubActions.ts | 37 ++++++++++--------- .../backend/src/server/common/AdminRoutes.ts | 2 +- .../src/server/common/AutoTestRoutes.ts | 14 ++++--- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/autotest/src/autotest/AutoTest.ts b/packages/autotest/src/autotest/AutoTest.ts index e433bbee5..d018fbb9e 100644 --- a/packages/autotest/src/autotest/AutoTest.ts +++ b/packages/autotest/src/autotest/AutoTest.ts @@ -243,7 +243,7 @@ export abstract class AutoTest implements IAutoTest { const tickQueue = function (queue: Queue): void { if (queue.length() > 0 && queue.hasCapacity() === true) { const info: ContainerInput = queue.scheduleNext(); - Log.info("AutoTest::tick::tickQueue(..) - starting job on: " + queue.getName() + "; deliv: " + + Log.info("AutoTest::tick::tickQueue(..) - starting: " + queue.getName() + "; deliv: " + info.delivId + '; repo: ' + info.target.repoId + '; SHA: ' + info.target.commitSHA); let gradingJob: GradingJob; diff --git a/packages/autotest/src/github/GitHubAutoTest.ts b/packages/autotest/src/github/GitHubAutoTest.ts index f9e768027..402d56940 100644 --- a/packages/autotest/src/github/GitHubAutoTest.ts +++ b/packages/autotest/src/github/GitHubAutoTest.ts @@ -282,9 +282,9 @@ export class GitHubAutoTest extends AutoTest implements IGitHubTestManager { */ protected async postToGitHub(target: CommitTarget, message: IGitHubMessage): Promise { if (typeof target.flags !== 'undefined' && target.flags.indexOf("#silent") >= 0) { - Log.info("GitHubAutoTest::postToGitHub(..) - #silent specified; NOT posting message to: " + message.url); + Log.info("GitHubAutoTest::postToGitHub(..) - #silent specified; NOT posting message; repo: " + target.repoId); } else { - Log.info("GitHubAutoTest::postToGitHub(..) - posting message to: " + message.url); + Log.info("GitHubAutoTest::postToGitHub(..) - posting; repo: " + target.repoId + "; sha: " + target.commitSHA); Log.trace("GitHubAutoTest::postToGitHub(..) - target: " + JSON.stringify(target)); Log.trace("GitHubAutoTest::postToGitHub(..) - message: " + JSON.stringify(message)); return await GitHubUtil.postMarkdownToGithub(message); diff --git a/packages/portal/backend/src/controllers/AdminController.ts b/packages/portal/backend/src/controllers/AdminController.ts index 55c64e966..eb216fc61 100644 --- a/packages/portal/backend/src/controllers/AdminController.ts +++ b/packages/portal/backend/src/controllers/AdminController.ts @@ -369,7 +369,7 @@ export class AdminController { public async performStudentWithdraw(): Promise { Log.info("AdminController::performStudentWithdraw() - start"); const gha = GitHubActions.getInstance(true); - const tc = new TeamController(); + // const tc = new TeamController(); // const teamNum = await tc.getTeamNumber('students'); // await gha.getTeamNumber('students'); // const registeredGithubIds = await gha.getTeamMembers(teamNum); const registeredGithubIds = await gha.getTeamMembers('students'); diff --git a/packages/portal/backend/src/controllers/GitHubActions.ts b/packages/portal/backend/src/controllers/GitHubActions.ts index 119fb417e..fdb86e934 100644 --- a/packages/portal/backend/src/controllers/GitHubActions.ts +++ b/packages/portal/backend/src/controllers/GitHubActions.ts @@ -573,41 +573,42 @@ export class GitHubActions implements IGitHubActions { let raw: any[] = []; const paginationPromises: any[] = []; - if (response.headers.has('link')) { + if (response.headers.has("link")) { // first save the responses from the first page: raw = body; let lastPage: number = -1; - const linkText = response.headers.get('link'); - Log.info('GitHubActions::handlePagination(..) - linkText: ' + linkText); - const linkParts = linkText.split(','); + const linkText = response.headers.get("link"); + Log.info("GitHubActions::handlePagination(..) - linkText: " + linkText); + const linkParts = linkText.split(","); for (const p of linkParts) { - const pparts = p.split(';'); - if (pparts[1].indexOf('last')) { - const pText = pparts[0].split('&page=')[1]; - Log.info('GitHubActions::handlePagination(..) - last page pText:_' + pText + '_; p: ' + p); + Log.info("GitHubActions::handlePagination(..) - linkParts: " + p); + const pparts = p.split(";"); + if (pparts[1].indexOf("last") >= 0) { + const pText = pparts[0].split("&page=")[1]; + Log.info("GitHubActions::handlePagination(..) - last page pText:_" + pText + "_; p: " + p); lastPage = Number(pText.match(/\d+/)[0]); - Log.info('GitHubActions::handlePagination(..) - last page: ' + lastPage); + Log.info("GitHubActions::handlePagination(..) - last page: " + lastPage); } } - let pageBase = ''; + let pageBase = ""; for (const p of linkParts) { - const pparts = p.split(';'); - if (pparts[1].indexOf('next')) { - let pText = pparts[0].split('&page=')[0].trim(); - Log.info('GitHubActions::handlePagination(..) - pt: ' + pText); + const pparts = p.split(";"); + if (pparts[1].indexOf("next") >= 0) { + let pText = pparts[0].split("&page=")[0].trim(); + Log.info("GitHubActions::handlePagination(..) - pt: " + pText); pText = pText.substring(1); pText = pText + "&page="; pageBase = pText; - Log.info('GitHubActions::handlePagination(..) - page base: ' + pageBase); + Log.info("GitHubActions::handlePagination(..) - page base: " + pageBase); } } Log.info("GitHubActions::handlePagination(..) - handling pagination; # pages: " + lastPage); for (let i = 2; i <= lastPage; i++) { const pageUri = pageBase + i; - Log.info('GitHubActions::handlePagination(..) - page to request: ' + pageUri); + Log.info("GitHubActions::handlePagination(..) - page to request: " + pageUri); uri = pageUri; // not sure why this is needed // NOTE: this needs to be slowed down to prevent DNS problems (issuing 10+ concurrent dns requests can be problematic) await Util.delay(100); @@ -1016,14 +1017,14 @@ export class GitHubActions implements IGitHubActions { const start = Date.now(); try { - // /orgs/:org/teams/:team_slug + // /orgs/{org}/teams/{team_slug}/members const uri = this.apiPath + "/orgs/" + this.org + "/teams/" + teamName + "/members"; const options: RequestInit = { method: 'GET', headers: { 'Authorization': this.gitHubAuthToken, 'User-Agent': this.gitHubUserName, - 'Accept': 'application/json' + 'Accept': 'application/vnd.github+json' } }; diff --git a/packages/portal/backend/src/server/common/AdminRoutes.ts b/packages/portal/backend/src/server/common/AdminRoutes.ts index 882b5a890..7a17a6b71 100644 --- a/packages/portal/backend/src/server/common/AdminRoutes.ts +++ b/packages/portal/backend/src/server/common/AdminRoutes.ts @@ -228,7 +228,7 @@ export default class AdminRoutes implements IREST { const ac = new AdminController(AdminRoutes.ghc); ac.getStudents().then(function(students) { - Log.trace('AdminRoutes::getStudents(..) - in then; # students: ' + students.length); + Log.info('AdminRoutes::getStudents(..) - done; # students: ' + students.length); const payload: StudentTransportPayload = {success: students}; res.send(payload); return next(); diff --git a/packages/portal/backend/src/server/common/AutoTestRoutes.ts b/packages/portal/backend/src/server/common/AutoTestRoutes.ts index 987c260fc..8a8073cd4 100644 --- a/packages/portal/backend/src/server/common/AutoTestRoutes.ts +++ b/packages/portal/backend/src/server/common/AutoTestRoutes.ts @@ -408,15 +408,15 @@ export class AutoTestRoutes implements IREST { } /** - * Forwards Webhook to AutoTest if it is from a valid host. Returns the processed body to GitHub - * so we can debug the contents in GitHub's webhook view, if needed. + * Forwards Webhook to AutoTest if it is from a valid host. Returns the processed body + * to GitHub, so we can debug the contents in GitHub's webhook view, if needed. * * @param req * @returns {Promise<{}>} */ private static async handleWebhook(req: any): Promise<{}> { - const config = Config.getInstance(); const headers = JSON.stringify(req.headers); + const config = Config.getInstance(); const atHost = config.getProp(ConfigKey.autotestUrl); const url = atHost + ':' + config.getProp(ConfigKey.autotestPort) + '/githubWebhook'; const options: RequestInit = { @@ -427,10 +427,14 @@ export class AutoTestRoutes implements IREST { const res = await fetch(url, options); if (res.ok) { Log.trace('AutoTestRouteHandler::handleWebhook(..) - success: ' + JSON.stringify(res.ok)); + Log.info('AutoTestRouteHandler::handleWebhook(..) - success'); return res.ok; + } else { + const err = await res.json(); + const msg = 'AutoTestRouteHandler::handleWebhook(..) - ERROR: ' + JSON.stringify(err.message); + Log.error(msg); + throw new Error(msg); } - const err = await res.json(); - throw new Error('AutoTestRouteHandler::handleWebhook(..) - ERROR: ' + JSON.stringify(err.message)); } public static async getDockerImages(req: any, res: any, next: any) { From 732eb0dcb88447db0c772bb526c6160c2113efd7 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sat, 24 Sep 2022 18:18:44 -0700 Subject: [PATCH 045/104] updated admin teams tab --- .../frontend/src/app/util/SortableTable.ts | 2 +- .../frontend/src/app/views/AdminTeamsTab.ts | 204 ++++++++++-------- 2 files changed, 121 insertions(+), 85 deletions(-) diff --git a/packages/portal/frontend/src/app/util/SortableTable.ts b/packages/portal/frontend/src/app/util/SortableTable.ts index fa9b259a2..7153c21ab 100644 --- a/packages/portal/frontend/src/app/util/SortableTable.ts +++ b/packages/portal/frontend/src/app/util/SortableTable.ts @@ -63,7 +63,7 @@ export class SortableTable { } /** - * Replaces all of the current rows. + * Replaces all the current rows. * * @param {TableCell[][]} rows */ diff --git a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts index 3eb259903..c83ef935e 100644 --- a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts +++ b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts @@ -14,6 +14,7 @@ import {AdminPage} from "./AdminPage"; import {AdminResultsTab} from "./AdminResultsTab"; import {AdminStudentsTab} from "./AdminStudentsTab"; import {AdminView} from "./AdminView"; +import {AdminDeliverablesTab} from "@frontend/views/AdminDeliverablesTab"; export class AdminTeamsTab extends AdminPage { @@ -28,11 +29,11 @@ export class AdminTeamsTab extends AdminPage { // called by reflection in renderPage public async init(opts: any): Promise { - Log.info('AdminTeamsTab::init(..) - start'); + Log.info('AdminTeamsTab::init(..) - start; opts: ' + JSON.stringify(opts)); const start = Date.now(); document.getElementById('teamsListTable').innerHTML = ''; // clear target - document.getElementById('teamsIndividualListTable').innerHTML = ''; // clear target + // document.getElementById('teamsIndividualListTable').innerHTML = ''; // clear target this.students = []; this.teams = []; @@ -49,10 +50,61 @@ export class AdminTeamsTab extends AdminPage { } } + const delivs = await AdminDeliverablesTab.getDeliverables(this.remote); this.repos = await AdminResultsTab.getRepositories(this.remote); this.teams = await AdminTeamsTab.getTeams(this.remote); this.students = await AdminStudentsTab.getStudents(this.remote); + const dStr = ['-None-']; + for (const deliv of delivs) { + dStr.push(deliv.id); + } + // opts = opts.sort(); + UI.setDropdownOptions('teamsListSelect', dStr, opts.delivId); + + const delivSelector = document.querySelector('#teamsListSelect') as HTMLSelectElement; + const statusSelector = document.querySelector('#teamsListStatusSelect') as HTMLSelectElement; + + const that = this; + + const updateTeamTable = function () { + const delivValue = delivSelector.value.valueOf(); + const statusValue = statusSelector.value.valueOf(); + Log.info("AdminTeamsTab::init(..)::updateTeamTable() - deliv: " + + delivValue + "; status: " + statusValue); + + if (statusValue === "formed") { + Log.info("AdminTeamsTab::init(..)::updateTeamTable() - rendering formed"); + that.renderTeams(that.teams, delivValue); // if cached data is ok + } else { + Log.info("AdminTeamsTab::init(..)::updateTeamTable() - rendering unformed"); + that.renderIndividuals(that.teams, that.students, delivValue); // if cached data is ok + } + }; + + delivSelector.onchange = function (evt) { + Log.info('AdminTeamsTab::init(..) - deliv changed'); + evt.stopPropagation(); // prevents list item expansion + updateTeamTable(); + // const val = delivSelector.value.valueOf(); + // + // // that.renderPage('AdminTeams', {labSection: val}); // if we need to re-fetch + // that.renderTeams(that.teams, val); // if cached data is ok + // that.renderIndividuals(that.teams, that.students, val); // if cached data is ok + }; + + statusSelector.onchange = function (evt) { + Log.info('AdminTeamsTab::init(..) - status changed'); + evt.stopPropagation(); // prevents list item expansion + + updateTeamTable(); + // const val = delivSelector.value.valueOf(); + // + // // that.renderPage('AdminTeams', {labSection: val}); // if we need to re-fetch + // that.renderTeams(that.teams, val); // if cached data is ok + // that.renderIndividuals(that.teams, that.students, val); // if cached data is ok + }; + this.renderTeams(this.teams, opts.delivId); this.renderIndividuals(this.teams, this.students, opts.delivId); @@ -67,64 +119,64 @@ export class AdminTeamsTab extends AdminPage { Log.trace("AdminTeamsTab::renderTeams(..) - start"); const headers: TableHeader[] = [ { - id: 'num', - text: '#', - sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). + id: 'num', + text: '#', + sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). defaultSort: false, // Whether the column is the default sort for the table. should only be true for one column. - sortDown: false, // Whether the column should initially sort descending or ascending. - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: false, // Whether the column should initially sort descending or ascending. + style: 'padding-left: 1em; padding-right: 1em;' }, { - id: 'id', - text: 'Team Id', - sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). + id: 'id', + text: 'Team Id', + sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). defaultSort: true, // Whether the column is the default sort for the table. should only be true for one column. - sortDown: false, // Whether the column should initially sort descending or ascending. - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: false, // Whether the column should initially sort descending or ascending. + style: 'padding-left: 1em; padding-right: 1em;' }, { - id: 'repo', - text: 'Repository', - sortable: true, + id: 'repo', + text: 'Repository', + sortable: true, defaultSort: false, - sortDown: false, - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: false, + style: 'padding-left: 1em; padding-right: 1em;' }, { - id: 'labs', - text: 'Labs', - sortable: true, + id: 'labs', + text: 'Labs', + sortable: true, defaultSort: false, - sortDown: false, - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: false, + style: 'padding-left: 1em; padding-right: 1em;' }, { - id: 'p1', - text: 'First Member', - sortable: true, + id: 'p1', + text: 'First Member', + sortable: true, defaultSort: false, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em;' }, { - id: 'p2', - text: 'Second Member', - sortable: true, + id: 'p2', + text: 'Second Member', + sortable: true, defaultSort: false, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em;' }, { - id: 'p3', - text: 'Third Member', - sortable: true, + id: 'p3', + text: 'Third Member', + sortable: true, defaultSort: false, - sortDown: true, - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: true, + style: 'padding-left: 1em; padding-right: 1em;' } ]; - let delivOptions = ['-None-']; + // let delivOptions = ['-None-']; const st = new SortableTable(headers, '#teamsListTable'); let listContainsStudents = false; @@ -177,9 +229,9 @@ export class AdminTeamsTab extends AdminPage { {value: p2, html: p2}, {value: p3, html: p3} ]; - if (delivOptions.indexOf(team.delivId) < 0 && team.delivId !== '' && team.delivId !== null) { - delivOptions.push(team.delivId); - } + // if (delivOptions.indexOf(team.delivId) < 0 && team.delivId !== '' && team.delivId !== null) { + // delivOptions.push(team.delivId); + // } if (delivId === team.delivId && team.people.length > 0) { count++; st.addRow(row); @@ -189,30 +241,13 @@ export class AdminTeamsTab extends AdminPage { st.generate(); - delivOptions = delivOptions.sort(); - UI.setDropdownOptions('teamsListSelect', delivOptions, delivId); - - const delivSelector = document.querySelector('#teamsListSelect') as HTMLSelectElement; - - const that = this; - delivSelector.onchange = function(evt) { - Log.info('AdminTeamsTab::renderTeams(..) - upload pressed'); - evt.stopPropagation(); // prevents list item expansion - - const val = delivSelector.value.valueOf(); - - // that.renderPage('AdminTeams', {labSection: val}); // if we need to re-fetch - that.renderTeams(that.teams, val); // if cached data is ok - that.renderIndividuals(that.teams, that.students, val); // if cached data is ok - }; - - if (st.numRows() > 0) { - UI.showSection('teamsListTable'); - UI.hideSection('teamsListTableNone'); - } else { - UI.hideSection('teamsListTable'); - UI.showSection('teamsListTableNone'); - } + // if (st.numRows() > 0) { + // UI.showSection('teamsListTable'); + // UI.hideSection('teamsListTableNone'); + // } else { + // UI.hideSection('teamsListTable'); + // UI.showSection('teamsListTableNone'); + // } } private getLabsCell(people: string[]): string { @@ -261,28 +296,29 @@ export class AdminTeamsTab extends AdminPage { } private renderIndividuals(teams: TeamTransport[], students: StudentTransport[], delivId: string): void { - Log.trace("AdminTeamsTab::renderTeams(..) - start"); + Log.trace("AdminTeamsTab::renderIndividuals(..) - start"); const headers: TableHeader[] = [ { - id: 'num', - text: '#', - sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). + id: 'num', + text: '#', + sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). defaultSort: false, // Whether the column is the default sort for the table. should only be true for one column. - sortDown: false, // Whether the column should initially sort descending or ascending. - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: false, // Whether the column should initially sort descending or ascending. + style: 'padding-left: 1em; padding-right: 1em;' }, { - id: 'id', - text: 'Student', - sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). + id: 'id', + text: 'Student', + sortable: true, // Whether the column is sortable (sometimes sorting does not make sense). defaultSort: true, // Whether the column is the default sort for the table. should only be true for one column. - sortDown: false, // Whether the column should initially sort descending or ascending. - style: 'padding-left: 1em; padding-right: 1em;' + sortDown: false, // Whether the column should initially sort descending or ascending. + style: 'padding-left: 1em; padding-right: 1em;' } ]; - const st = new SortableTable(headers, '#teamsIndividualListTable'); + // const st = new SortableTable(headers, '#teamsIndividualListTable'); + const st = new SortableTable(headers, '#teamsListTable'); const studentsOnTeams: string[] = []; for (const team of teams) { @@ -320,13 +356,13 @@ export class AdminTeamsTab extends AdminPage { st.generate(); - if (st.numRows() > 0) { - UI.showSection('teamsIndividualListTable'); - UI.hideSection('teamsIndividualListTableNone'); - } else { - UI.hideSection('teamsIndividualListTable'); - UI.showSection('teamsIndividualListTableNone'); - } + // if (st.numRows() > 0) { + // UI.showSection('teamsIndividualListTable'); + // UI.hideSection('teamsIndividualListTableNone'); + // } else { + // UI.hideSection('teamsIndividualListTable'); + // UI.showSection('teamsIndividualListTableNone'); + // } } From 30bbf013c1a92cd6365bd48bb1c04736d79e9400 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sat, 24 Sep 2022 21:58:22 -0700 Subject: [PATCH 046/104] Show lab on unformed teams --- .../portal/frontend/src/app/views/AdminTeamsTab.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts index c83ef935e..0449cbea2 100644 --- a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts +++ b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts @@ -307,6 +307,14 @@ export class AdminTeamsTab extends AdminPage { sortDown: false, // Whether the column should initially sort descending or ascending. style: 'padding-left: 1em; padding-right: 1em;' }, + { + id: 'lab', + text: 'Lab', + sortable: true, + defaultSort: false, + sortDown: false, + style: 'padding-left: 1em; padding-right: 1em;' + }, { id: 'id', text: 'Student', @@ -342,9 +350,11 @@ export class AdminTeamsTab extends AdminPage { studentHTML = student.firstName + ' ' + student.lastName + ' ' + student.githubId + ' (' + student.id + ')'; } + const lab = student.labId ?? ''; const row: TableCell[] = [ {value: count, html: count++ + ''}, + {value: lab, html: lab}, {value: student.id, html: studentHTML} ]; if (delivId !== '-None-') { From e9fa9294e63af4292b3dfd224884e076346bd86a Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sat, 24 Sep 2022 22:06:15 -0700 Subject: [PATCH 047/104] Make scores above 100 render correctly on Grades page --- .../frontend/src/app/views/AdminGradesTab.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/portal/frontend/src/app/views/AdminGradesTab.ts b/packages/portal/frontend/src/app/views/AdminGradesTab.ts index e3957193e..e4d1df9a9 100644 --- a/packages/portal/frontend/src/app/views/AdminGradesTab.ts +++ b/packages/portal/frontend/src/app/views/AdminGradesTab.ts @@ -129,15 +129,12 @@ export class AdminGradesTab extends AdminPage { if (grade.delivId === deliv.id) { const hoverComment = AdminGradesTab.makeHTMLSafe(grade.comment); // let score = ''; - let score: number | string = ''; + let scoreText: string = ''; let scorePrepend = ''; if (grade.score !== null && grade.score >= 0) { - score = grade.score; - if (score === 100) { - score = "100.00"; - } else { + scoreText = grade.score.toFixed(2); + if (grade.score < 100) { // two decimal places - score = score.toFixed(2); // prepend space (not 100) scorePrepend = " " + scorePrepend; if (grade.score < 10) { @@ -145,17 +142,16 @@ export class AdminGradesTab extends AdminPage { scorePrepend = " " + scorePrepend; } } - // score = grade.score + ''; } let html; - if (score !== '' && grade.URL !== null) { - html = scorePrepend + `${score}`; - } else if (score !== '' && grade.URL === null) { - html = `
${score}
`; + if (scoreText !== '' && grade.URL !== null) { + html = scorePrepend + `${scoreText}`; + } else if (scoreText !== '' && grade.URL === null) { + html = `
${scoreText}
`; } else { - html = score; + html = scoreText; } - tableCell = {value: score, html}; + tableCell = {value: scoreText, html}; } } } From 63c5b7142f74c25ff4e4c7afe8d2426e2ac85ca8 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sat, 24 Sep 2022 22:16:09 -0700 Subject: [PATCH 048/104] Restrict teams view to delivs with teams --- .../portal/frontend/src/app/views/AdminTeamsTab.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts index 0449cbea2..27b5c035a 100644 --- a/packages/portal/frontend/src/app/views/AdminTeamsTab.ts +++ b/packages/portal/frontend/src/app/views/AdminTeamsTab.ts @@ -43,14 +43,15 @@ export class AdminTeamsTab extends AdminPage { this.course = await AdminView.getCourse(this.remote); if (typeof opts.delivId === 'undefined') { - if (this.course.defaultDeliverableId !== null) { - opts.delivId = this.course.defaultDeliverableId; - } else { - opts.delivId = '-None-'; - } + // if (this.course.defaultDeliverableId !== null) { + // opts.delivId = this.course.defaultDeliverableId; + // } else { + opts.delivId = '-None-'; + // } } - const delivs = await AdminDeliverablesTab.getDeliverables(this.remote); + const delivs = (await AdminDeliverablesTab.getDeliverables(this.remote)) + .filter((deliv) => deliv.shouldProvision); this.repos = await AdminResultsTab.getRepositories(this.remote); this.teams = await AdminTeamsTab.getTeams(this.remote); this.students = await AdminStudentsTab.getStudents(this.remote); From 975266e5982b78f2a2b10eedb395ca0e33cc4757 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sun, 25 Sep 2022 08:32:07 -0700 Subject: [PATCH 049/104] Remove schedule from docs --- docs/instructor/autotest.md | 4 ++-- docs/instructor/features.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/instructor/autotest.md b/docs/instructor/autotest.md index dd5aea529..4f3773816 100644 --- a/docs/instructor/autotest.md +++ b/docs/instructor/autotest.md @@ -21,9 +21,9 @@ AutoTest listens for `push` and `comment` events in repositories managed by Auto AutoTest can compute feedback either when a GitHub push event (e.g., a `git push`) is received or when a user makes a comment on a commit (e.g., they use the GitHub web interface to make a comment that references the AutoTest bot). The name of the bot is configurable, but we will use `@autobot` for the remainder of this document. These messages should take the form `@autobot [flags]`. For example `@autobot #d1` or `@autobot #d4`. Flags do not need to be provided unless needed; the complete list of flags includes: -* `#schedule` Schedules a commit for grading in the future when the student's quota is available again. For instance, by default calling `@autobot #d2` when the student still has 6 hours remaining before they can request again does not actually queue the submission for grading. By calling `@autobot #d2 #schedule` the submission will be automatically graded when the student's quota allows. Note: each student has only one `#schedule` slot; only the most recent `#schedule` event will be serviced; once this is complete the slot is available again. +[//]: # (* `#schedule` Schedules a commit for grading in the future when the student's quota is available again. For instance, by default calling `@autobot #d2` when the student still has 6 hours remaining before they can request again does not actually queue the submission for grading. By calling `@autobot #d2 #schedule` the submission will be automatically graded when the student's quota allows. Note: each student has only one `#schedule` slot; only the most recent `#schedule` event will be serviced; once this is complete the slot is available again.) -* `#unschedule` Unschedules a commit; this is not strictly necessary as servicing a scheduled grading submission does this automatically (as does calling `#schedule` on another commit), but this is a convenience method that ensures the student does not have a scheduled event requested. +[//]: # (* `#unschedule` Unschedules a commit; this is not strictly necessary as servicing a scheduled grading submission does this automatically (as does calling `#schedule` on another commit), but this is a convenience method that ensures the student does not have a scheduled event requested.) * `#check` Checks to ensure a commit has been queued for grading. This is often used by students who want to confirm that their submission is in fact on the grading queue. diff --git a/docs/instructor/features.md b/docs/instructor/features.md index d4f931237..ae8725566 100644 --- a/docs/instructor/features.md +++ b/docs/instructor/features.md @@ -9,7 +9,7 @@ - [Feedback for an Assignment](#feedback-for-an-assignment) - [Feedback for an Assignment that Fails to Compile](#feedback-for-an-assignment-that-fails-to-compile) - [Feedback for an Assignment Before Allowed Time](#feedback-for-an-assignment-before-allowed-time) - - [Feedback for an Assignment Using #schedule Feature](#feedback-for-an-assignment-using-schedule-feature) + - [Admin Role Examples](#admin-role-examples) - [Request Feedback When Student Cannot](#request-feedback-when-student-cannot) - [Force a Re-Grade](#force-a-re-grade) @@ -69,11 +69,11 @@ A student pushes code to a repository and requests feedback for the commit displ -#### Feedback for an Assignment Using #schedule Feature +[//]: # (#### Feedback for an Assignment Using #schedule Feature) -A student pushes code to a repository but cannot request grade feedback because the student is within the configured delay period. The student schedules feedback by using the `#schedule` flag with the specified deliverable in a grade request to `AutoBot`. +[//]: # (A student pushes code to a repository but cannot request grade feedback because the student is within the configured delay period. The student schedules feedback by using the `#schedule` flag with the specified deliverable in a grade request to `AutoBot`.) - +[//]: # () ### Admin Role Examples From 63b4d64b3be5711e27d29990a57c8f7ace448832 Mon Sep 17 00:00:00 2001 From: Reid Holmes Date: Sun, 25 Sep 2022 14:13:00 -0700 Subject: [PATCH 050/104] Pin to yarn v1.18.0 resolution policies --- .yarn/releases/yarn-1.18.0.cjs | 147155 ++++++++++++++++++++++++++++++ .yarnrc | 5 + 2 files changed, 147160 insertions(+) create mode 100755 .yarn/releases/yarn-1.18.0.cjs create mode 100644 .yarnrc diff --git a/.yarn/releases/yarn-1.18.0.cjs b/.yarn/releases/yarn-1.18.0.cjs new file mode 100755 index 000000000..97e2a1966 --- /dev/null +++ b/.yarn/releases/yarn-1.18.0.cjs @@ -0,0 +1,147155 @@ +#!/usr/bin/env node +module.exports = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 549); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +module.exports = require("path"); + +/***/ }), +/* 1 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (immutable) */ __webpack_exports__["a"] = __extends; +/* unused harmony export __assign */ +/* unused harmony export __rest */ +/* unused harmony export __decorate */ +/* unused harmony export __param */ +/* unused harmony export __metadata */ +/* unused harmony export __awaiter */ +/* unused harmony export __generator */ +/* unused harmony export __exportStar */ +/* unused harmony export __values */ +/* unused harmony export __read */ +/* unused harmony export __spread */ +/* unused harmony export __await */ +/* unused harmony export __asyncGenerator */ +/* unused harmony export __asyncDelegator */ +/* unused harmony export __asyncValues */ +/* unused harmony export __makeTemplateObject */ +/* unused harmony export __importStar */ +/* unused harmony export __importDefault */ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +/* global Reflect, Promise */ + +var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); +}; + +function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +} + +var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + } + return __assign.apply(this, arguments); +} + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0) + t[p[i]] = s[p[i]]; + return t; +} + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +function __param(paramIndex, decorator) { + return function (target, key) { decorator(target, key, paramIndex); } +} + +function __metadata(metadataKey, metadataValue) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); +} + +function __awaiter(thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +function __generator(thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +} + +function __exportStar(m, exports) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} + +function __values(o) { + var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; + if (m) return m.call(o); + return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; +} + +function __read(o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +} + +function __spread() { + for (var ar = [], i = 0; i < arguments.length; i++) + ar = ar.concat(__read(arguments[i])); + return ar; +} + +function __await(v) { + return this instanceof __await ? (this.v = v, this) : new __await(v); +} + +function __asyncGenerator(thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var g = generator.apply(thisArg, _arguments || []), i, q = []; + return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; + function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } + function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } + function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } + function fulfill(value) { resume("next", value); } + function reject(value) { resume("throw", value); } + function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } +} + +function __asyncDelegator(o) { + var i, p; + return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i; + function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; } +} + +function __asyncValues(o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +} + +function __makeTemplateObject(cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; + +function __importStar(mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result.default = mod; + return result; +} + +function __importDefault(mod) { + return (mod && mod.__esModule) ? mod : { default: mod }; +} + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +exports.__esModule = true; + +var _promise = __webpack_require__(227); + +var _promise2 = _interopRequireDefault(_promise); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new _promise2.default(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return _promise2.default.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; +}; + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +module.exports = require("util"); + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.getFirstSuitableFolder = exports.readFirstAvailableStream = exports.makeTempDir = exports.hardlinksWork = exports.writeFilePreservingEol = exports.getFileSizeOnDisk = exports.walk = exports.symlink = exports.find = exports.readJsonAndFile = exports.readJson = exports.readFileAny = exports.hardlinkBulk = exports.copyBulk = exports.unlink = exports.glob = exports.link = exports.chmod = exports.lstat = exports.exists = exports.mkdirp = exports.stat = exports.access = exports.rename = exports.readdir = exports.realpath = exports.readlink = exports.writeFile = exports.open = exports.readFileBuffer = exports.lockQueue = exports.constants = undefined; + +var _asyncToGenerator2; + +function _load_asyncToGenerator() { + return _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(2)); +} + +let buildActionsForCopy = (() => { + var _ref = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, events, possibleExtraneous, reporter) { + + // + let build = (() => { + var _ref5 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + const src = data.src, + dest = data.dest, + type = data.type; + + const onFresh = data.onFresh || noop; + const onDone = data.onDone || noop; + + // TODO https://github.com/yarnpkg/yarn/issues/3751 + // related to bundled dependencies handling + if (files.has(dest.toLowerCase())) { + reporter.verbose(`The case-insensitive file ${dest} shouldn't be copied twice in one bulk copy`); + } else { + files.add(dest.toLowerCase()); + } + + if (type === 'symlink') { + yield mkdirp((_path || _load_path()).default.dirname(dest)); + onFresh(); + actions.symlink.push({ + dest, + linkname: src + }); + onDone(); + return; + } + + if (events.ignoreBasenames.indexOf((_path || _load_path()).default.basename(src)) >= 0) { + // ignored file + return; + } + + const srcStat = yield lstat(src); + let srcFiles; + + if (srcStat.isDirectory()) { + srcFiles = yield readdir(src); + } + + let destStat; + try { + // try accessing the destination + destStat = yield lstat(dest); + } catch (e) { + // proceed if destination doesn't exist, otherwise error + if (e.code !== 'ENOENT') { + throw e; + } + } + + // if destination exists + if (destStat) { + const bothSymlinks = srcStat.isSymbolicLink() && destStat.isSymbolicLink(); + const bothFolders = srcStat.isDirectory() && destStat.isDirectory(); + const bothFiles = srcStat.isFile() && destStat.isFile(); + + // EINVAL access errors sometimes happen which shouldn't because node shouldn't be giving + // us modes that aren't valid. investigate this, it's generally safe to proceed. + + /* if (srcStat.mode !== destStat.mode) { + try { + await access(dest, srcStat.mode); + } catch (err) {} + } */ + + if (bothFiles && artifactFiles.has(dest)) { + // this file gets changed during build, likely by a custom install script. Don't bother checking it. + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipArtifact', src)); + return; + } + + if (bothFiles && srcStat.size === destStat.size && (0, (_fsNormalized || _load_fsNormalized()).fileDatesEqual)(srcStat.mtime, destStat.mtime)) { + // we can safely assume this is the same file + onDone(); + reporter.verbose(reporter.lang('verboseFileSkip', src, dest, srcStat.size, +srcStat.mtime)); + return; + } + + if (bothSymlinks) { + const srcReallink = yield readlink(src); + if (srcReallink === (yield readlink(dest))) { + // if both symlinks are the same then we can continue on + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipSymlink', src, dest, srcReallink)); + return; + } + } + + if (bothFolders) { + // mark files that aren't in this folder as possibly extraneous + const destFiles = yield readdir(dest); + invariant(srcFiles, 'src files not initialised'); + + for (var _iterator4 = destFiles, _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { + var _ref6; + + if (_isArray4) { + if (_i4 >= _iterator4.length) break; + _ref6 = _iterator4[_i4++]; + } else { + _i4 = _iterator4.next(); + if (_i4.done) break; + _ref6 = _i4.value; + } + + const file = _ref6; + + if (srcFiles.indexOf(file) < 0) { + const loc = (_path || _load_path()).default.join(dest, file); + possibleExtraneous.add(loc); + + if ((yield lstat(loc)).isDirectory()) { + for (var _iterator5 = yield readdir(loc), _isArray5 = Array.isArray(_iterator5), _i5 = 0, _iterator5 = _isArray5 ? _iterator5 : _iterator5[Symbol.iterator]();;) { + var _ref7; + + if (_isArray5) { + if (_i5 >= _iterator5.length) break; + _ref7 = _iterator5[_i5++]; + } else { + _i5 = _iterator5.next(); + if (_i5.done) break; + _ref7 = _i5.value; + } + + const file = _ref7; + + possibleExtraneous.add((_path || _load_path()).default.join(loc, file)); + } + } + } + } + } + } + + if (destStat && destStat.isSymbolicLink()) { + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(dest); + destStat = null; + } + + if (srcStat.isSymbolicLink()) { + onFresh(); + const linkname = yield readlink(src); + actions.symlink.push({ + dest, + linkname + }); + onDone(); + } else if (srcStat.isDirectory()) { + if (!destStat) { + reporter.verbose(reporter.lang('verboseFileFolder', dest)); + yield mkdirp(dest); + } + + const destParts = dest.split((_path || _load_path()).default.sep); + while (destParts.length) { + files.add(destParts.join((_path || _load_path()).default.sep).toLowerCase()); + destParts.pop(); + } + + // push all files to queue + invariant(srcFiles, 'src files not initialised'); + let remaining = srcFiles.length; + if (!remaining) { + onDone(); + } + for (var _iterator6 = srcFiles, _isArray6 = Array.isArray(_iterator6), _i6 = 0, _iterator6 = _isArray6 ? _iterator6 : _iterator6[Symbol.iterator]();;) { + var _ref8; + + if (_isArray6) { + if (_i6 >= _iterator6.length) break; + _ref8 = _iterator6[_i6++]; + } else { + _i6 = _iterator6.next(); + if (_i6.done) break; + _ref8 = _i6.value; + } + + const file = _ref8; + + queue.push({ + dest: (_path || _load_path()).default.join(dest, file), + onFresh, + onDone: function (_onDone) { + function onDone() { + return _onDone.apply(this, arguments); + } + + onDone.toString = function () { + return _onDone.toString(); + }; + + return onDone; + }(function () { + if (--remaining === 0) { + onDone(); + } + }), + src: (_path || _load_path()).default.join(src, file) + }); + } + } else if (srcStat.isFile()) { + onFresh(); + actions.file.push({ + src, + dest, + atime: srcStat.atime, + mtime: srcStat.mtime, + mode: srcStat.mode + }); + onDone(); + } else { + throw new Error(`unsure how to copy this: ${src}`); + } + }); + + return function build(_x5) { + return _ref5.apply(this, arguments); + }; + })(); + + const artifactFiles = new Set(events.artifactFiles || []); + const files = new Set(); + + // initialise events + for (var _iterator = queue, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref2; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref2 = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref2 = _i.value; + } + + const item = _ref2; + + const onDone = item.onDone; + item.onDone = function () { + events.onProgress(item.dest); + if (onDone) { + onDone(); + } + }; + } + events.onStart(queue.length); + + // start building actions + const actions = { + file: [], + symlink: [], + link: [] + }; + + // custom concurrency logic as we're always executing stacks of CONCURRENT_QUEUE_ITEMS queue items + // at a time due to the requirement to push items onto the queue + while (queue.length) { + const items = queue.splice(0, CONCURRENT_QUEUE_ITEMS); + yield Promise.all(items.map(build)); + } + + // simulate the existence of some files to prevent considering them extraneous + for (var _iterator2 = artifactFiles, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { + var _ref3; + + if (_isArray2) { + if (_i2 >= _iterator2.length) break; + _ref3 = _iterator2[_i2++]; + } else { + _i2 = _iterator2.next(); + if (_i2.done) break; + _ref3 = _i2.value; + } + + const file = _ref3; + + if (possibleExtraneous.has(file)) { + reporter.verbose(reporter.lang('verboseFilePhantomExtraneous', file)); + possibleExtraneous.delete(file); + } + } + + for (var _iterator3 = possibleExtraneous, _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { + var _ref4; + + if (_isArray3) { + if (_i3 >= _iterator3.length) break; + _ref4 = _iterator3[_i3++]; + } else { + _i3 = _iterator3.next(); + if (_i3.done) break; + _ref4 = _i3.value; + } + + const loc = _ref4; + + if (files.has(loc.toLowerCase())) { + possibleExtraneous.delete(loc); + } + } + + return actions; + }); + + return function buildActionsForCopy(_x, _x2, _x3, _x4) { + return _ref.apply(this, arguments); + }; +})(); + +let buildActionsForHardlink = (() => { + var _ref9 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, events, possibleExtraneous, reporter) { + + // + let build = (() => { + var _ref13 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + const src = data.src, + dest = data.dest; + + const onFresh = data.onFresh || noop; + const onDone = data.onDone || noop; + if (files.has(dest.toLowerCase())) { + // Fixes issue https://github.com/yarnpkg/yarn/issues/2734 + // When bulk hardlinking we have A -> B structure that we want to hardlink to A1 -> B1, + // package-linker passes that modules A1 and B1 need to be hardlinked, + // the recursive linking algorithm of A1 ends up scheduling files in B1 to be linked twice which will case + // an exception. + onDone(); + return; + } + files.add(dest.toLowerCase()); + + if (events.ignoreBasenames.indexOf((_path || _load_path()).default.basename(src)) >= 0) { + // ignored file + return; + } + + const srcStat = yield lstat(src); + let srcFiles; + + if (srcStat.isDirectory()) { + srcFiles = yield readdir(src); + } + + const destExists = yield exists(dest); + if (destExists) { + const destStat = yield lstat(dest); + + const bothSymlinks = srcStat.isSymbolicLink() && destStat.isSymbolicLink(); + const bothFolders = srcStat.isDirectory() && destStat.isDirectory(); + const bothFiles = srcStat.isFile() && destStat.isFile(); + + if (srcStat.mode !== destStat.mode) { + try { + yield access(dest, srcStat.mode); + } catch (err) { + // EINVAL access errors sometimes happen which shouldn't because node shouldn't be giving + // us modes that aren't valid. investigate this, it's generally safe to proceed. + reporter.verbose(err); + } + } + + if (bothFiles && artifactFiles.has(dest)) { + // this file gets changed during build, likely by a custom install script. Don't bother checking it. + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipArtifact', src)); + return; + } + + // correct hardlink + if (bothFiles && srcStat.ino !== null && srcStat.ino === destStat.ino) { + onDone(); + reporter.verbose(reporter.lang('verboseFileSkip', src, dest, srcStat.ino)); + return; + } + + if (bothSymlinks) { + const srcReallink = yield readlink(src); + if (srcReallink === (yield readlink(dest))) { + // if both symlinks are the same then we can continue on + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipSymlink', src, dest, srcReallink)); + return; + } + } + + if (bothFolders) { + // mark files that aren't in this folder as possibly extraneous + const destFiles = yield readdir(dest); + invariant(srcFiles, 'src files not initialised'); + + for (var _iterator10 = destFiles, _isArray10 = Array.isArray(_iterator10), _i10 = 0, _iterator10 = _isArray10 ? _iterator10 : _iterator10[Symbol.iterator]();;) { + var _ref14; + + if (_isArray10) { + if (_i10 >= _iterator10.length) break; + _ref14 = _iterator10[_i10++]; + } else { + _i10 = _iterator10.next(); + if (_i10.done) break; + _ref14 = _i10.value; + } + + const file = _ref14; + + if (srcFiles.indexOf(file) < 0) { + const loc = (_path || _load_path()).default.join(dest, file); + possibleExtraneous.add(loc); + + if ((yield lstat(loc)).isDirectory()) { + for (var _iterator11 = yield readdir(loc), _isArray11 = Array.isArray(_iterator11), _i11 = 0, _iterator11 = _isArray11 ? _iterator11 : _iterator11[Symbol.iterator]();;) { + var _ref15; + + if (_isArray11) { + if (_i11 >= _iterator11.length) break; + _ref15 = _iterator11[_i11++]; + } else { + _i11 = _iterator11.next(); + if (_i11.done) break; + _ref15 = _i11.value; + } + + const file = _ref15; + + possibleExtraneous.add((_path || _load_path()).default.join(loc, file)); + } + } + } + } + } + } + + if (srcStat.isSymbolicLink()) { + onFresh(); + const linkname = yield readlink(src); + actions.symlink.push({ + dest, + linkname + }); + onDone(); + } else if (srcStat.isDirectory()) { + reporter.verbose(reporter.lang('verboseFileFolder', dest)); + yield mkdirp(dest); + + const destParts = dest.split((_path || _load_path()).default.sep); + while (destParts.length) { + files.add(destParts.join((_path || _load_path()).default.sep).toLowerCase()); + destParts.pop(); + } + + // push all files to queue + invariant(srcFiles, 'src files not initialised'); + let remaining = srcFiles.length; + if (!remaining) { + onDone(); + } + for (var _iterator12 = srcFiles, _isArray12 = Array.isArray(_iterator12), _i12 = 0, _iterator12 = _isArray12 ? _iterator12 : _iterator12[Symbol.iterator]();;) { + var _ref16; + + if (_isArray12) { + if (_i12 >= _iterator12.length) break; + _ref16 = _iterator12[_i12++]; + } else { + _i12 = _iterator12.next(); + if (_i12.done) break; + _ref16 = _i12.value; + } + + const file = _ref16; + + queue.push({ + onFresh, + src: (_path || _load_path()).default.join(src, file), + dest: (_path || _load_path()).default.join(dest, file), + onDone: function (_onDone2) { + function onDone() { + return _onDone2.apply(this, arguments); + } + + onDone.toString = function () { + return _onDone2.toString(); + }; + + return onDone; + }(function () { + if (--remaining === 0) { + onDone(); + } + }) + }); + } + } else if (srcStat.isFile()) { + onFresh(); + actions.link.push({ + src, + dest, + removeDest: destExists + }); + onDone(); + } else { + throw new Error(`unsure how to copy this: ${src}`); + } + }); + + return function build(_x10) { + return _ref13.apply(this, arguments); + }; + })(); + + const artifactFiles = new Set(events.artifactFiles || []); + const files = new Set(); + + // initialise events + for (var _iterator7 = queue, _isArray7 = Array.isArray(_iterator7), _i7 = 0, _iterator7 = _isArray7 ? _iterator7 : _iterator7[Symbol.iterator]();;) { + var _ref10; + + if (_isArray7) { + if (_i7 >= _iterator7.length) break; + _ref10 = _iterator7[_i7++]; + } else { + _i7 = _iterator7.next(); + if (_i7.done) break; + _ref10 = _i7.value; + } + + const item = _ref10; + + const onDone = item.onDone || noop; + item.onDone = function () { + events.onProgress(item.dest); + onDone(); + }; + } + events.onStart(queue.length); + + // start building actions + const actions = { + file: [], + symlink: [], + link: [] + }; + + // custom concurrency logic as we're always executing stacks of CONCURRENT_QUEUE_ITEMS queue items + // at a time due to the requirement to push items onto the queue + while (queue.length) { + const items = queue.splice(0, CONCURRENT_QUEUE_ITEMS); + yield Promise.all(items.map(build)); + } + + // simulate the existence of some files to prevent considering them extraneous + for (var _iterator8 = artifactFiles, _isArray8 = Array.isArray(_iterator8), _i8 = 0, _iterator8 = _isArray8 ? _iterator8 : _iterator8[Symbol.iterator]();;) { + var _ref11; + + if (_isArray8) { + if (_i8 >= _iterator8.length) break; + _ref11 = _iterator8[_i8++]; + } else { + _i8 = _iterator8.next(); + if (_i8.done) break; + _ref11 = _i8.value; + } + + const file = _ref11; + + if (possibleExtraneous.has(file)) { + reporter.verbose(reporter.lang('verboseFilePhantomExtraneous', file)); + possibleExtraneous.delete(file); + } + } + + for (var _iterator9 = possibleExtraneous, _isArray9 = Array.isArray(_iterator9), _i9 = 0, _iterator9 = _isArray9 ? _iterator9 : _iterator9[Symbol.iterator]();;) { + var _ref12; + + if (_isArray9) { + if (_i9 >= _iterator9.length) break; + _ref12 = _iterator9[_i9++]; + } else { + _i9 = _iterator9.next(); + if (_i9.done) break; + _ref12 = _i9.value; + } + + const loc = _ref12; + + if (files.has(loc.toLowerCase())) { + possibleExtraneous.delete(loc); + } + } + + return actions; + }); + + return function buildActionsForHardlink(_x6, _x7, _x8, _x9) { + return _ref9.apply(this, arguments); + }; +})(); + +let copyBulk = exports.copyBulk = (() => { + var _ref17 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, reporter, _events) { + const events = { + onStart: _events && _events.onStart || noop, + onProgress: _events && _events.onProgress || noop, + possibleExtraneous: _events ? _events.possibleExtraneous : new Set(), + ignoreBasenames: _events && _events.ignoreBasenames || [], + artifactFiles: _events && _events.artifactFiles || [] + }; + + const actions = yield buildActionsForCopy(queue, events, events.possibleExtraneous, reporter); + events.onStart(actions.file.length + actions.symlink.length + actions.link.length); + + const fileActions = actions.file; + + const currentlyWriting = new Map(); + + yield (_promise || _load_promise()).queue(fileActions, (() => { + var _ref18 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + let writePromise; + while (writePromise = currentlyWriting.get(data.dest)) { + yield writePromise; + } + + reporter.verbose(reporter.lang('verboseFileCopy', data.src, data.dest)); + const copier = (0, (_fsNormalized || _load_fsNormalized()).copyFile)(data, function () { + return currentlyWriting.delete(data.dest); + }); + currentlyWriting.set(data.dest, copier); + events.onProgress(data.dest); + return copier; + }); + + return function (_x14) { + return _ref18.apply(this, arguments); + }; + })(), CONCURRENT_QUEUE_ITEMS); + + // we need to copy symlinks last as they could reference files we were copying + const symlinkActions = actions.symlink; + yield (_promise || _load_promise()).queue(symlinkActions, function (data) { + const linkname = (_path || _load_path()).default.resolve((_path || _load_path()).default.dirname(data.dest), data.linkname); + reporter.verbose(reporter.lang('verboseFileSymlink', data.dest, linkname)); + return symlink(linkname, data.dest); + }); + }); + + return function copyBulk(_x11, _x12, _x13) { + return _ref17.apply(this, arguments); + }; +})(); + +let hardlinkBulk = exports.hardlinkBulk = (() => { + var _ref19 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, reporter, _events) { + const events = { + onStart: _events && _events.onStart || noop, + onProgress: _events && _events.onProgress || noop, + possibleExtraneous: _events ? _events.possibleExtraneous : new Set(), + artifactFiles: _events && _events.artifactFiles || [], + ignoreBasenames: [] + }; + + const actions = yield buildActionsForHardlink(queue, events, events.possibleExtraneous, reporter); + events.onStart(actions.file.length + actions.symlink.length + actions.link.length); + + const fileActions = actions.link; + + yield (_promise || _load_promise()).queue(fileActions, (() => { + var _ref20 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + reporter.verbose(reporter.lang('verboseFileLink', data.src, data.dest)); + if (data.removeDest) { + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(data.dest); + } + yield link(data.src, data.dest); + }); + + return function (_x18) { + return _ref20.apply(this, arguments); + }; + })(), CONCURRENT_QUEUE_ITEMS); + + // we need to copy symlinks last as they could reference files we were copying + const symlinkActions = actions.symlink; + yield (_promise || _load_promise()).queue(symlinkActions, function (data) { + const linkname = (_path || _load_path()).default.resolve((_path || _load_path()).default.dirname(data.dest), data.linkname); + reporter.verbose(reporter.lang('verboseFileSymlink', data.dest, linkname)); + return symlink(linkname, data.dest); + }); + }); + + return function hardlinkBulk(_x15, _x16, _x17) { + return _ref19.apply(this, arguments); + }; +})(); + +let readFileAny = exports.readFileAny = (() => { + var _ref21 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (files) { + for (var _iterator13 = files, _isArray13 = Array.isArray(_iterator13), _i13 = 0, _iterator13 = _isArray13 ? _iterator13 : _iterator13[Symbol.iterator]();;) { + var _ref22; + + if (_isArray13) { + if (_i13 >= _iterator13.length) break; + _ref22 = _iterator13[_i13++]; + } else { + _i13 = _iterator13.next(); + if (_i13.done) break; + _ref22 = _i13.value; + } + + const file = _ref22; + + if (yield exists(file)) { + return readFile(file); + } + } + return null; + }); + + return function readFileAny(_x19) { + return _ref21.apply(this, arguments); + }; +})(); + +let readJson = exports.readJson = (() => { + var _ref23 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (loc) { + return (yield readJsonAndFile(loc)).object; + }); + + return function readJson(_x20) { + return _ref23.apply(this, arguments); + }; +})(); + +let readJsonAndFile = exports.readJsonAndFile = (() => { + var _ref24 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (loc) { + const file = yield readFile(loc); + try { + return { + object: (0, (_map || _load_map()).default)(JSON.parse(stripBOM(file))), + content: file + }; + } catch (err) { + err.message = `${loc}: ${err.message}`; + throw err; + } + }); + + return function readJsonAndFile(_x21) { + return _ref24.apply(this, arguments); + }; +})(); + +let find = exports.find = (() => { + var _ref25 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (filename, dir) { + const parts = dir.split((_path || _load_path()).default.sep); + + while (parts.length) { + const loc = parts.concat(filename).join((_path || _load_path()).default.sep); + + if (yield exists(loc)) { + return loc; + } else { + parts.pop(); + } + } + + return false; + }); + + return function find(_x22, _x23) { + return _ref25.apply(this, arguments); + }; +})(); + +let symlink = exports.symlink = (() => { + var _ref26 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (src, dest) { + if (process.platform !== 'win32') { + // use relative paths otherwise which will be retained if the directory is moved + src = (_path || _load_path()).default.relative((_path || _load_path()).default.dirname(dest), src); + // When path.relative returns an empty string for the current directory, we should instead use + // '.', which is a valid fs.symlink target. + src = src || '.'; + } + + try { + const stats = yield lstat(dest); + if (stats.isSymbolicLink()) { + const resolved = dest; + if (resolved === src) { + return; + } + } + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + // We use rimraf for unlink which never throws an ENOENT on missing target + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(dest); + + if (process.platform === 'win32') { + // use directory junctions if possible on win32, this requires absolute paths + yield fsSymlink(src, dest, 'junction'); + } else { + yield fsSymlink(src, dest); + } + }); + + return function symlink(_x24, _x25) { + return _ref26.apply(this, arguments); + }; +})(); + +let walk = exports.walk = (() => { + var _ref27 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (dir, relativeDir, ignoreBasenames = new Set()) { + let files = []; + + let filenames = yield readdir(dir); + if (ignoreBasenames.size) { + filenames = filenames.filter(function (name) { + return !ignoreBasenames.has(name); + }); + } + + for (var _iterator14 = filenames, _isArray14 = Array.isArray(_iterator14), _i14 = 0, _iterator14 = _isArray14 ? _iterator14 : _iterator14[Symbol.iterator]();;) { + var _ref28; + + if (_isArray14) { + if (_i14 >= _iterator14.length) break; + _ref28 = _iterator14[_i14++]; + } else { + _i14 = _iterator14.next(); + if (_i14.done) break; + _ref28 = _i14.value; + } + + const name = _ref28; + + const relative = relativeDir ? (_path || _load_path()).default.join(relativeDir, name) : name; + const loc = (_path || _load_path()).default.join(dir, name); + const stat = yield lstat(loc); + + files.push({ + relative, + basename: name, + absolute: loc, + mtime: +stat.mtime + }); + + if (stat.isDirectory()) { + files = files.concat((yield walk(loc, relative, ignoreBasenames))); + } + } + + return files; + }); + + return function walk(_x26, _x27) { + return _ref27.apply(this, arguments); + }; +})(); + +let getFileSizeOnDisk = exports.getFileSizeOnDisk = (() => { + var _ref29 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (loc) { + const stat = yield lstat(loc); + const size = stat.size, + blockSize = stat.blksize; + + + return Math.ceil(size / blockSize) * blockSize; + }); + + return function getFileSizeOnDisk(_x28) { + return _ref29.apply(this, arguments); + }; +})(); + +let getEolFromFile = (() => { + var _ref30 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (path) { + if (!(yield exists(path))) { + return undefined; + } + + const buffer = yield readFileBuffer(path); + + for (let i = 0; i < buffer.length; ++i) { + if (buffer[i] === cr) { + return '\r\n'; + } + if (buffer[i] === lf) { + return '\n'; + } + } + return undefined; + }); + + return function getEolFromFile(_x29) { + return _ref30.apply(this, arguments); + }; +})(); + +let writeFilePreservingEol = exports.writeFilePreservingEol = (() => { + var _ref31 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (path, data) { + const eol = (yield getEolFromFile(path)) || (_os || _load_os()).default.EOL; + if (eol !== '\n') { + data = data.replace(/\n/g, eol); + } + yield writeFile(path, data); + }); + + return function writeFilePreservingEol(_x30, _x31) { + return _ref31.apply(this, arguments); + }; +})(); + +let hardlinksWork = exports.hardlinksWork = (() => { + var _ref32 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (dir) { + const filename = 'test-file' + Math.random(); + const file = (_path || _load_path()).default.join(dir, filename); + const fileLink = (_path || _load_path()).default.join(dir, filename + '-link'); + try { + yield writeFile(file, 'test'); + yield link(file, fileLink); + } catch (err) { + return false; + } finally { + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(file); + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(fileLink); + } + return true; + }); + + return function hardlinksWork(_x32) { + return _ref32.apply(this, arguments); + }; +})(); + +// not a strict polyfill for Node's fs.mkdtemp + + +let makeTempDir = exports.makeTempDir = (() => { + var _ref33 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (prefix) { + const dir = (_path || _load_path()).default.join((_os || _load_os()).default.tmpdir(), `yarn-${prefix || ''}-${Date.now()}-${Math.random()}`); + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(dir); + yield mkdirp(dir); + return dir; + }); + + return function makeTempDir(_x33) { + return _ref33.apply(this, arguments); + }; +})(); + +let readFirstAvailableStream = exports.readFirstAvailableStream = (() => { + var _ref34 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (paths) { + for (var _iterator15 = paths, _isArray15 = Array.isArray(_iterator15), _i15 = 0, _iterator15 = _isArray15 ? _iterator15 : _iterator15[Symbol.iterator]();;) { + var _ref35; + + if (_isArray15) { + if (_i15 >= _iterator15.length) break; + _ref35 = _iterator15[_i15++]; + } else { + _i15 = _iterator15.next(); + if (_i15.done) break; + _ref35 = _i15.value; + } + + const path = _ref35; + + try { + const fd = yield open(path, 'r'); + return (_fs || _load_fs()).default.createReadStream(path, { fd }); + } catch (err) { + // Try the next one + } + } + return null; + }); + + return function readFirstAvailableStream(_x34) { + return _ref34.apply(this, arguments); + }; +})(); + +let getFirstSuitableFolder = exports.getFirstSuitableFolder = (() => { + var _ref36 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (paths, mode = constants.W_OK | constants.X_OK) { + const result = { + skipped: [], + folder: null + }; + + for (var _iterator16 = paths, _isArray16 = Array.isArray(_iterator16), _i16 = 0, _iterator16 = _isArray16 ? _iterator16 : _iterator16[Symbol.iterator]();;) { + var _ref37; + + if (_isArray16) { + if (_i16 >= _iterator16.length) break; + _ref37 = _iterator16[_i16++]; + } else { + _i16 = _iterator16.next(); + if (_i16.done) break; + _ref37 = _i16.value; + } + + const folder = _ref37; + + try { + yield mkdirp(folder); + yield access(folder, mode); + + result.folder = folder; + + return result; + } catch (error) { + result.skipped.push({ + error, + folder + }); + } + } + return result; + }); + + return function getFirstSuitableFolder(_x35) { + return _ref36.apply(this, arguments); + }; +})(); + +exports.copy = copy; +exports.readFile = readFile; +exports.readFileRaw = readFileRaw; +exports.normalizeOS = normalizeOS; + +var _fs; + +function _load_fs() { + return _fs = _interopRequireDefault(__webpack_require__(5)); +} + +var _glob; + +function _load_glob() { + return _glob = _interopRequireDefault(__webpack_require__(99)); +} + +var _os; + +function _load_os() { + return _os = _interopRequireDefault(__webpack_require__(49)); +} + +var _path; + +function _load_path() { + return _path = _interopRequireDefault(__webpack_require__(0)); +} + +var _blockingQueue; + +function _load_blockingQueue() { + return _blockingQueue = _interopRequireDefault(__webpack_require__(110)); +} + +var _promise; + +function _load_promise() { + return _promise = _interopRequireWildcard(__webpack_require__(50)); +} + +var _promise2; + +function _load_promise2() { + return _promise2 = __webpack_require__(50); +} + +var _map; + +function _load_map() { + return _map = _interopRequireDefault(__webpack_require__(29)); +} + +var _fsNormalized; + +function _load_fsNormalized() { + return _fsNormalized = __webpack_require__(218); +} + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const constants = exports.constants = typeof (_fs || _load_fs()).default.constants !== 'undefined' ? (_fs || _load_fs()).default.constants : { + R_OK: (_fs || _load_fs()).default.R_OK, + W_OK: (_fs || _load_fs()).default.W_OK, + X_OK: (_fs || _load_fs()).default.X_OK +}; + +const lockQueue = exports.lockQueue = new (_blockingQueue || _load_blockingQueue()).default('fs lock'); + +const readFileBuffer = exports.readFileBuffer = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readFile); +const open = exports.open = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.open); +const writeFile = exports.writeFile = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.writeFile); +const readlink = exports.readlink = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readlink); +const realpath = exports.realpath = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.realpath); +const readdir = exports.readdir = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readdir); +const rename = exports.rename = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.rename); +const access = exports.access = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.access); +const stat = exports.stat = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.stat); +const mkdirp = exports.mkdirp = (0, (_promise2 || _load_promise2()).promisify)(__webpack_require__(145)); +const exists = exports.exists = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.exists, true); +const lstat = exports.lstat = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.lstat); +const chmod = exports.chmod = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.chmod); +const link = exports.link = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.link); +const glob = exports.glob = (0, (_promise2 || _load_promise2()).promisify)((_glob || _load_glob()).default); +exports.unlink = (_fsNormalized || _load_fsNormalized()).unlink; + +// fs.copyFile uses the native file copying instructions on the system, performing much better +// than any JS-based solution and consumes fewer resources. Repeated testing to fine tune the +// concurrency level revealed 128 as the sweet spot on a quad-core, 16 CPU Intel system with SSD. + +const CONCURRENT_QUEUE_ITEMS = (_fs || _load_fs()).default.copyFile ? 128 : 4; + +const fsSymlink = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.symlink); +const invariant = __webpack_require__(9); +const stripBOM = __webpack_require__(160); + +const noop = () => {}; + +function copy(src, dest, reporter) { + return copyBulk([{ src, dest }], reporter); +} + +function _readFile(loc, encoding) { + return new Promise((resolve, reject) => { + (_fs || _load_fs()).default.readFile(loc, encoding, function (err, content) { + if (err) { + reject(err); + } else { + resolve(content); + } + }); + }); +} + +function readFile(loc) { + return _readFile(loc, 'utf8').then(normalizeOS); +} + +function readFileRaw(loc) { + return _readFile(loc, 'binary'); +} + +function normalizeOS(body) { + return body.replace(/\r\n/g, '\n'); +} + +const cr = '\r'.charCodeAt(0); +const lf = '\n'.charCodeAt(0); + +/***/ }), +/* 5 */ +/***/ (function(module, exports) { + +module.exports = require("fs"); + +/***/ }), +/* 6 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +class MessageError extends Error { + constructor(msg, code) { + super(msg); + this.code = code; + } + +} + +exports.MessageError = MessageError; +class ProcessSpawnError extends MessageError { + constructor(msg, code, process) { + super(msg, code); + this.process = process; + } + +} + +exports.ProcessSpawnError = ProcessSpawnError; +class SecurityError extends MessageError {} + +exports.SecurityError = SecurityError; +class ProcessTermError extends MessageError {} + +exports.ProcessTermError = ProcessTermError; +class ResponseError extends Error { + constructor(msg, responseCode) { + super(msg); + this.responseCode = responseCode; + } + +} + +exports.ResponseError = ResponseError; +class OneTimePasswordError extends Error {} +exports.OneTimePasswordError = OneTimePasswordError; + +/***/ }), +/* 7 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Subscriber; }); +/* unused harmony export SafeSubscriber */ +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_tslib__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__util_isFunction__ = __webpack_require__(154); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__Observer__ = __webpack_require__(420); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__Subscription__ = __webpack_require__(25); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__internal_symbol_rxSubscriber__ = __webpack_require__(321); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__config__ = __webpack_require__(185); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__util_hostReportError__ = __webpack_require__(323); +/** PURE_IMPORTS_START tslib,_util_isFunction,_Observer,_Subscription,_internal_symbol_rxSubscriber,_config,_util_hostReportError PURE_IMPORTS_END */ + + + + + + + +var Subscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](Subscriber, _super); + function Subscriber(destinationOrNext, error, complete) { + var _this = _super.call(this) || this; + _this.syncErrorValue = null; + _this.syncErrorThrown = false; + _this.syncErrorThrowable = false; + _this.isStopped = false; + _this._parentSubscription = null; + switch (arguments.length) { + case 0: + _this.destination = __WEBPACK_IMPORTED_MODULE_2__Observer__["a" /* empty */]; + break; + case 1: + if (!destinationOrNext) { + _this.destination = __WEBPACK_IMPORTED_MODULE_2__Observer__["a" /* empty */]; + break; + } + if (typeof destinationOrNext === 'object') { + if (destinationOrNext instanceof Subscriber) { + _this.syncErrorThrowable = destinationOrNext.syncErrorThrowable; + _this.destination = destinationOrNext; + destinationOrNext.add(_this); + } + else { + _this.syncErrorThrowable = true; + _this.destination = new SafeSubscriber(_this, destinationOrNext); + } + break; + } + default: + _this.syncErrorThrowable = true; + _this.destination = new SafeSubscriber(_this, destinationOrNext, error, complete); + break; + } + return _this; + } + Subscriber.prototype[__WEBPACK_IMPORTED_MODULE_4__internal_symbol_rxSubscriber__["a" /* rxSubscriber */]] = function () { return this; }; + Subscriber.create = function (next, error, complete) { + var subscriber = new Subscriber(next, error, complete); + subscriber.syncErrorThrowable = false; + return subscriber; + }; + Subscriber.prototype.next = function (value) { + if (!this.isStopped) { + this._next(value); + } + }; + Subscriber.prototype.error = function (err) { + if (!this.isStopped) { + this.isStopped = true; + this._error(err); + } + }; + Subscriber.prototype.complete = function () { + if (!this.isStopped) { + this.isStopped = true; + this._complete(); + } + }; + Subscriber.prototype.unsubscribe = function () { + if (this.closed) { + return; + } + this.isStopped = true; + _super.prototype.unsubscribe.call(this); + }; + Subscriber.prototype._next = function (value) { + this.destination.next(value); + }; + Subscriber.prototype._error = function (err) { + this.destination.error(err); + this.unsubscribe(); + }; + Subscriber.prototype._complete = function () { + this.destination.complete(); + this.unsubscribe(); + }; + Subscriber.prototype._unsubscribeAndRecycle = function () { + var _a = this, _parent = _a._parent, _parents = _a._parents; + this._parent = null; + this._parents = null; + this.unsubscribe(); + this.closed = false; + this.isStopped = false; + this._parent = _parent; + this._parents = _parents; + this._parentSubscription = null; + return this; + }; + return Subscriber; +}(__WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */])); + +var SafeSubscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](SafeSubscriber, _super); + function SafeSubscriber(_parentSubscriber, observerOrNext, error, complete) { + var _this = _super.call(this) || this; + _this._parentSubscriber = _parentSubscriber; + var next; + var context = _this; + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_isFunction__["a" /* isFunction */])(observerOrNext)) { + next = observerOrNext; + } + else if (observerOrNext) { + next = observerOrNext.next; + error = observerOrNext.error; + complete = observerOrNext.complete; + if (observerOrNext !== __WEBPACK_IMPORTED_MODULE_2__Observer__["a" /* empty */]) { + context = Object.create(observerOrNext); + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_isFunction__["a" /* isFunction */])(context.unsubscribe)) { + _this.add(context.unsubscribe.bind(context)); + } + context.unsubscribe = _this.unsubscribe.bind(_this); + } + } + _this._context = context; + _this._next = next; + _this._error = error; + _this._complete = complete; + return _this; + } + SafeSubscriber.prototype.next = function (value) { + if (!this.isStopped && this._next) { + var _parentSubscriber = this._parentSubscriber; + if (!__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) { + this.__tryOrUnsub(this._next, value); + } + else if (this.__tryOrSetError(_parentSubscriber, this._next, value)) { + this.unsubscribe(); + } + } + }; + SafeSubscriber.prototype.error = function (err) { + if (!this.isStopped) { + var _parentSubscriber = this._parentSubscriber; + var useDeprecatedSynchronousErrorHandling = __WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling; + if (this._error) { + if (!useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) { + this.__tryOrUnsub(this._error, err); + this.unsubscribe(); + } + else { + this.__tryOrSetError(_parentSubscriber, this._error, err); + this.unsubscribe(); + } + } + else if (!_parentSubscriber.syncErrorThrowable) { + this.unsubscribe(); + if (useDeprecatedSynchronousErrorHandling) { + throw err; + } + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + } + else { + if (useDeprecatedSynchronousErrorHandling) { + _parentSubscriber.syncErrorValue = err; + _parentSubscriber.syncErrorThrown = true; + } + else { + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + } + this.unsubscribe(); + } + } + }; + SafeSubscriber.prototype.complete = function () { + var _this = this; + if (!this.isStopped) { + var _parentSubscriber = this._parentSubscriber; + if (this._complete) { + var wrappedComplete = function () { return _this._complete.call(_this._context); }; + if (!__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) { + this.__tryOrUnsub(wrappedComplete); + this.unsubscribe(); + } + else { + this.__tryOrSetError(_parentSubscriber, wrappedComplete); + this.unsubscribe(); + } + } + else { + this.unsubscribe(); + } + } + }; + SafeSubscriber.prototype.__tryOrUnsub = function (fn, value) { + try { + fn.call(this._context, value); + } + catch (err) { + this.unsubscribe(); + if (__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + throw err; + } + else { + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + } + } + }; + SafeSubscriber.prototype.__tryOrSetError = function (parent, fn, value) { + if (!__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + throw new Error('bad call'); + } + try { + fn.call(this._context, value); + } + catch (err) { + if (__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + parent.syncErrorValue = err; + parent.syncErrorThrown = true; + return true; + } + else { + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + return true; + } + } + return false; + }; + SafeSubscriber.prototype._unsubscribe = function () { + var _parentSubscriber = this._parentSubscriber; + this._context = null; + this._parentSubscriber = null; + _parentSubscriber.unsubscribe(); + }; + return SafeSubscriber; +}(Subscriber)); + +//# sourceMappingURL=Subscriber.js.map + + +/***/ }), +/* 8 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.getPathKey = getPathKey; +const os = __webpack_require__(49); +const path = __webpack_require__(0); +const userHome = __webpack_require__(66).default; + +var _require = __webpack_require__(225); + +const getCacheDir = _require.getCacheDir, + getConfigDir = _require.getConfigDir, + getDataDir = _require.getDataDir; + +const isWebpackBundle = __webpack_require__(278); + +const DEPENDENCY_TYPES = exports.DEPENDENCY_TYPES = ['devDependencies', 'dependencies', 'optionalDependencies', 'peerDependencies']; +const OWNED_DEPENDENCY_TYPES = exports.OWNED_DEPENDENCY_TYPES = ['devDependencies', 'dependencies', 'optionalDependencies']; + +const RESOLUTIONS = exports.RESOLUTIONS = 'resolutions'; +const MANIFEST_FIELDS = exports.MANIFEST_FIELDS = [RESOLUTIONS, ...DEPENDENCY_TYPES]; + +const SUPPORTED_NODE_VERSIONS = exports.SUPPORTED_NODE_VERSIONS = '^4.8.0 || ^5.7.0 || ^6.2.2 || >=8.0.0'; + +const YARN_REGISTRY = exports.YARN_REGISTRY = 'https://registry.yarnpkg.com'; +const NPM_REGISTRY_RE = exports.NPM_REGISTRY_RE = /https?:\/\/registry\.npmjs\.org/g; + +const YARN_DOCS = exports.YARN_DOCS = 'https://yarnpkg.com/en/docs/cli/'; +const YARN_INSTALLER_SH = exports.YARN_INSTALLER_SH = 'https://yarnpkg.com/install.sh'; +const YARN_INSTALLER_MSI = exports.YARN_INSTALLER_MSI = 'https://yarnpkg.com/latest.msi'; + +const SELF_UPDATE_VERSION_URL = exports.SELF_UPDATE_VERSION_URL = 'https://yarnpkg.com/latest-version'; + +// cache version, bump whenever we make backwards incompatible changes +const CACHE_VERSION = exports.CACHE_VERSION = 4; + +// lockfile version, bump whenever we make backwards incompatible changes +const LOCKFILE_VERSION = exports.LOCKFILE_VERSION = 1; + +// max amount of network requests to perform concurrently +const NETWORK_CONCURRENCY = exports.NETWORK_CONCURRENCY = 8; + +// HTTP timeout used when downloading packages +const NETWORK_TIMEOUT = exports.NETWORK_TIMEOUT = 30 * 1000; // in milliseconds + +// max amount of child processes to execute concurrently +const CHILD_CONCURRENCY = exports.CHILD_CONCURRENCY = 5; + +const REQUIRED_PACKAGE_KEYS = exports.REQUIRED_PACKAGE_KEYS = ['name', 'version', '_uid']; + +function getPreferredCacheDirectories() { + const preferredCacheDirectories = [getCacheDir()]; + + if (process.getuid) { + // $FlowFixMe: process.getuid exists, dammit + preferredCacheDirectories.push(path.join(os.tmpdir(), `.yarn-cache-${process.getuid()}`)); + } + + preferredCacheDirectories.push(path.join(os.tmpdir(), `.yarn-cache`)); + + return preferredCacheDirectories; +} + +const PREFERRED_MODULE_CACHE_DIRECTORIES = exports.PREFERRED_MODULE_CACHE_DIRECTORIES = getPreferredCacheDirectories(); +const CONFIG_DIRECTORY = exports.CONFIG_DIRECTORY = getConfigDir(); +const DATA_DIRECTORY = exports.DATA_DIRECTORY = getDataDir(); +const LINK_REGISTRY_DIRECTORY = exports.LINK_REGISTRY_DIRECTORY = path.join(DATA_DIRECTORY, 'link'); +const GLOBAL_MODULE_DIRECTORY = exports.GLOBAL_MODULE_DIRECTORY = path.join(DATA_DIRECTORY, 'global'); + +const NODE_BIN_PATH = exports.NODE_BIN_PATH = process.execPath; +const YARN_BIN_PATH = exports.YARN_BIN_PATH = getYarnBinPath(); + +// Webpack needs to be configured with node.__dirname/__filename = false +function getYarnBinPath() { + if (isWebpackBundle) { + return __filename; + } else { + return path.join(__dirname, '..', 'bin', 'yarn.js'); + } +} + +const NODE_MODULES_FOLDER = exports.NODE_MODULES_FOLDER = 'node_modules'; +const NODE_PACKAGE_JSON = exports.NODE_PACKAGE_JSON = 'package.json'; + +const PNP_FILENAME = exports.PNP_FILENAME = '.pnp.js'; + +const POSIX_GLOBAL_PREFIX = exports.POSIX_GLOBAL_PREFIX = `${process.env.DESTDIR || ''}/usr/local`; +const FALLBACK_GLOBAL_PREFIX = exports.FALLBACK_GLOBAL_PREFIX = path.join(userHome, '.yarn'); + +const META_FOLDER = exports.META_FOLDER = '.yarn-meta'; +const INTEGRITY_FILENAME = exports.INTEGRITY_FILENAME = '.yarn-integrity'; +const LOCKFILE_FILENAME = exports.LOCKFILE_FILENAME = 'yarn.lock'; +const METADATA_FILENAME = exports.METADATA_FILENAME = '.yarn-metadata.json'; +const TARBALL_FILENAME = exports.TARBALL_FILENAME = '.yarn-tarball.tgz'; +const CLEAN_FILENAME = exports.CLEAN_FILENAME = '.yarnclean'; + +const NPM_LOCK_FILENAME = exports.NPM_LOCK_FILENAME = 'package-lock.json'; +const NPM_SHRINKWRAP_FILENAME = exports.NPM_SHRINKWRAP_FILENAME = 'npm-shrinkwrap.json'; + +const DEFAULT_INDENT = exports.DEFAULT_INDENT = ' '; +const SINGLE_INSTANCE_PORT = exports.SINGLE_INSTANCE_PORT = 31997; +const SINGLE_INSTANCE_FILENAME = exports.SINGLE_INSTANCE_FILENAME = '.yarn-single-instance'; + +const ENV_PATH_KEY = exports.ENV_PATH_KEY = getPathKey(process.platform, process.env); + +function getPathKey(platform, env) { + let pathKey = 'PATH'; + + // windows calls its path "Path" usually, but this is not guaranteed. + if (platform === 'win32') { + pathKey = 'Path'; + + for (const key in env) { + if (key.toLowerCase() === 'path') { + pathKey = key; + } + } + } + + return pathKey; +} + +const VERSION_COLOR_SCHEME = exports.VERSION_COLOR_SCHEME = { + major: 'red', + premajor: 'red', + minor: 'yellow', + preminor: 'yellow', + patch: 'green', + prepatch: 'green', + prerelease: 'red', + unchanged: 'white', + unknown: 'red' +}; + +/***/ }), +/* 9 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + + +/** + * Use invariant() to assert state which your program assumes to be true. + * + * Provide sprintf-style format (only %s is supported) and arguments + * to provide information about what broke and what you were + * expecting. + * + * The invariant message will be stripped in production, but the invariant + * will remain to ensure logic does not differ in production. + */ + +var NODE_ENV = process.env.NODE_ENV; + +var invariant = function(condition, format, a, b, c, d, e, f) { + if (NODE_ENV !== 'production') { + if (format === undefined) { + throw new Error('invariant requires an error message argument'); + } + } + + if (!condition) { + var error; + if (format === undefined) { + error = new Error( + 'Minified exception occurred; use the non-minified dev environment ' + + 'for the full error message and additional helpful warnings.' + ); + } else { + var args = [a, b, c, d, e, f]; + var argIndex = 0; + error = new Error( + format.replace(/%s/g, function() { return args[argIndex++]; }) + ); + error.name = 'Invariant Violation'; + } + + error.framesToPop = 1; // we don't care about invariant's own frame + throw error; + } +}; + +module.exports = invariant; + + +/***/ }), +/* 10 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var YAMLException = __webpack_require__(54); + +var TYPE_CONSTRUCTOR_OPTIONS = [ + 'kind', + 'resolve', + 'construct', + 'instanceOf', + 'predicate', + 'represent', + 'defaultStyle', + 'styleAliases' +]; + +var YAML_NODE_KINDS = [ + 'scalar', + 'sequence', + 'mapping' +]; + +function compileStyleAliases(map) { + var result = {}; + + if (map !== null) { + Object.keys(map).forEach(function (style) { + map[style].forEach(function (alias) { + result[String(alias)] = style; + }); + }); + } + + return result; +} + +function Type(tag, options) { + options = options || {}; + + Object.keys(options).forEach(function (name) { + if (TYPE_CONSTRUCTOR_OPTIONS.indexOf(name) === -1) { + throw new YAMLException('Unknown option "' + name + '" is met in definition of "' + tag + '" YAML type.'); + } + }); + + // TODO: Add tag format check. + this.tag = tag; + this.kind = options['kind'] || null; + this.resolve = options['resolve'] || function () { return true; }; + this.construct = options['construct'] || function (data) { return data; }; + this.instanceOf = options['instanceOf'] || null; + this.predicate = options['predicate'] || null; + this.represent = options['represent'] || null; + this.defaultStyle = options['defaultStyle'] || null; + this.styleAliases = compileStyleAliases(options['styleAliases'] || null); + + if (YAML_NODE_KINDS.indexOf(this.kind) === -1) { + throw new YAMLException('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.'); + } +} + +module.exports = Type; + + +/***/ }), +/* 11 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Observable; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util_canReportError__ = __webpack_require__(322); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__util_toSubscriber__ = __webpack_require__(932); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__internal_symbol_observable__ = __webpack_require__(117); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__util_pipe__ = __webpack_require__(324); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__config__ = __webpack_require__(185); +/** PURE_IMPORTS_START _util_canReportError,_util_toSubscriber,_internal_symbol_observable,_util_pipe,_config PURE_IMPORTS_END */ + + + + + +var Observable = /*@__PURE__*/ (function () { + function Observable(subscribe) { + this._isScalar = false; + if (subscribe) { + this._subscribe = subscribe; + } + } + Observable.prototype.lift = function (operator) { + var observable = new Observable(); + observable.source = this; + observable.operator = operator; + return observable; + }; + Observable.prototype.subscribe = function (observerOrNext, error, complete) { + var operator = this.operator; + var sink = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_toSubscriber__["a" /* toSubscriber */])(observerOrNext, error, complete); + if (operator) { + operator.call(sink, this.source); + } + else { + sink.add(this.source || (__WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].useDeprecatedSynchronousErrorHandling && !sink.syncErrorThrowable) ? + this._subscribe(sink) : + this._trySubscribe(sink)); + } + if (__WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + if (sink.syncErrorThrowable) { + sink.syncErrorThrowable = false; + if (sink.syncErrorThrown) { + throw sink.syncErrorValue; + } + } + } + return sink; + }; + Observable.prototype._trySubscribe = function (sink) { + try { + return this._subscribe(sink); + } + catch (err) { + if (__WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + sink.syncErrorThrown = true; + sink.syncErrorValue = err; + } + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__util_canReportError__["a" /* canReportError */])(sink)) { + sink.error(err); + } + else { + console.warn(err); + } + } + }; + Observable.prototype.forEach = function (next, promiseCtor) { + var _this = this; + promiseCtor = getPromiseCtor(promiseCtor); + return new promiseCtor(function (resolve, reject) { + var subscription; + subscription = _this.subscribe(function (value) { + try { + next(value); + } + catch (err) { + reject(err); + if (subscription) { + subscription.unsubscribe(); + } + } + }, reject, resolve); + }); + }; + Observable.prototype._subscribe = function (subscriber) { + var source = this.source; + return source && source.subscribe(subscriber); + }; + Observable.prototype[__WEBPACK_IMPORTED_MODULE_2__internal_symbol_observable__["a" /* observable */]] = function () { + return this; + }; + Observable.prototype.pipe = function () { + var operations = []; + for (var _i = 0; _i < arguments.length; _i++) { + operations[_i] = arguments[_i]; + } + if (operations.length === 0) { + return this; + } + return __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_3__util_pipe__["b" /* pipeFromArray */])(operations)(this); + }; + Observable.prototype.toPromise = function (promiseCtor) { + var _this = this; + promiseCtor = getPromiseCtor(promiseCtor); + return new promiseCtor(function (resolve, reject) { + var value; + _this.subscribe(function (x) { return value = x; }, function (err) { return reject(err); }, function () { return resolve(value); }); + }); + }; + Observable.create = function (subscribe) { + return new Observable(subscribe); + }; + return Observable; +}()); + +function getPromiseCtor(promiseCtor) { + if (!promiseCtor) { + promiseCtor = __WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].Promise || Promise; + } + if (!promiseCtor) { + throw new Error('no Promise impl found'); + } + return promiseCtor; +} +//# sourceMappingURL=Observable.js.map + + +/***/ }), +/* 12 */ +/***/ (function(module, exports) { + +module.exports = require("crypto"); + +/***/ }), +/* 13 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return OuterSubscriber; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_tslib__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Subscriber__ = __webpack_require__(7); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +var OuterSubscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](OuterSubscriber, _super); + function OuterSubscriber() { + return _super !== null && _super.apply(this, arguments) || this; + } + OuterSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.destination.next(innerValue); + }; + OuterSubscriber.prototype.notifyError = function (error, innerSub) { + this.destination.error(error); + }; + OuterSubscriber.prototype.notifyComplete = function (innerSub) { + this.destination.complete(); + }; + return OuterSubscriber; +}(__WEBPACK_IMPORTED_MODULE_1__Subscriber__["a" /* Subscriber */])); + +//# sourceMappingURL=OuterSubscriber.js.map + + +/***/ }), +/* 14 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (immutable) */ __webpack_exports__["a"] = subscribeToResult; +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__InnerSubscriber__ = __webpack_require__(84); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__subscribeTo__ = __webpack_require__(446); +/** PURE_IMPORTS_START _InnerSubscriber,_subscribeTo PURE_IMPORTS_END */ + + +function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, destination) { + if (destination === void 0) { + destination = new __WEBPACK_IMPORTED_MODULE_0__InnerSubscriber__["a" /* InnerSubscriber */](outerSubscriber, outerValue, outerIndex); + } + if (destination.closed) { + return; + } + return __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__subscribeTo__["a" /* subscribeTo */])(result)(destination); +} +//# sourceMappingURL=subscribeToResult.js.map + + +/***/ }), +/* 15 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/* eslint-disable node/no-deprecated-api */ + + + +var buffer = __webpack_require__(64) +var Buffer = buffer.Buffer + +var safer = {} + +var key + +for (key in buffer) { + if (!buffer.hasOwnProperty(key)) continue + if (key === 'SlowBuffer' || key === 'Buffer') continue + safer[key] = buffer[key] +} + +var Safer = safer.Buffer = {} +for (key in Buffer) { + if (!Buffer.hasOwnProperty(key)) continue + if (key === 'allocUnsafe' || key === 'allocUnsafeSlow') continue + Safer[key] = Buffer[key] +} + +safer.Buffer.prototype = Buffer.prototype + +if (!Safer.from || Safer.from === Uint8Array.from) { + Safer.from = function (value, encodingOrOffset, length) { + if (typeof value === 'number') { + throw new TypeError('The "value" argument must not be of type number. Received type ' + typeof value) + } + if (value && typeof value.length === 'undefined') { + throw new TypeError('The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + typeof value) + } + return Buffer(value, encodingOrOffset, length) + } +} + +if (!Safer.alloc) { + Safer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('The "size" argument must be of type number. Received type ' + typeof size) + } + if (size < 0 || size >= 2 * (1 << 30)) { + throw new RangeError('The value "' + size + '" is invalid for option "size"') + } + var buf = Buffer(size) + if (!fill || fill.length === 0) { + buf.fill(0) + } else if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + return buf + } +} + +if (!safer.kStringMaxLength) { + try { + safer.kStringMaxLength = process.binding('buffer').kStringMaxLength + } catch (e) { + // we can't determine kStringMaxLength in environments where process.binding + // is unsupported, so let's not set it + } +} + +if (!safer.constants) { + safer.constants = { + MAX_LENGTH: safer.kMaxLength + } + if (safer.kStringMaxLength) { + safer.constants.MAX_STRING_LENGTH = safer.kStringMaxLength + } +} + +module.exports = safer + + +/***/ }), +/* 16 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright (c) 2012, Mark Cavage. All rights reserved. +// Copyright 2015 Joyent, Inc. + +var assert = __webpack_require__(28); +var Stream = __webpack_require__(23).Stream; +var util = __webpack_require__(3); + + +///--- Globals + +/* JSSTYLED */ +var UUID_REGEXP = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/; + + +///--- Internal + +function _capitalize(str) { + return (str.charAt(0).toUpperCase() + str.slice(1)); +} + +function _toss(name, expected, oper, arg, actual) { + throw new assert.AssertionError({ + message: util.format('%s (%s) is required', name, expected), + actual: (actual === undefined) ? typeof (arg) : actual(arg), + expected: expected, + operator: oper || '===', + stackStartFunction: _toss.caller + }); +} + +function _getClass(arg) { + return (Object.prototype.toString.call(arg).slice(8, -1)); +} + +function noop() { + // Why even bother with asserts? +} + + +///--- Exports + +var types = { + bool: { + check: function (arg) { return typeof (arg) === 'boolean'; } + }, + func: { + check: function (arg) { return typeof (arg) === 'function'; } + }, + string: { + check: function (arg) { return typeof (arg) === 'string'; } + }, + object: { + check: function (arg) { + return typeof (arg) === 'object' && arg !== null; + } + }, + number: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg); + } + }, + finite: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg) && isFinite(arg); + } + }, + buffer: { + check: function (arg) { return Buffer.isBuffer(arg); }, + operator: 'Buffer.isBuffer' + }, + array: { + check: function (arg) { return Array.isArray(arg); }, + operator: 'Array.isArray' + }, + stream: { + check: function (arg) { return arg instanceof Stream; }, + operator: 'instanceof', + actual: _getClass + }, + date: { + check: function (arg) { return arg instanceof Date; }, + operator: 'instanceof', + actual: _getClass + }, + regexp: { + check: function (arg) { return arg instanceof RegExp; }, + operator: 'instanceof', + actual: _getClass + }, + uuid: { + check: function (arg) { + return typeof (arg) === 'string' && UUID_REGEXP.test(arg); + }, + operator: 'isUUID' + } +}; + +function _setExports(ndebug) { + var keys = Object.keys(types); + var out; + + /* re-export standard assert */ + if (process.env.NODE_NDEBUG) { + out = noop; + } else { + out = function (arg, msg) { + if (!arg) { + _toss(msg, 'true', arg); + } + }; + } + + /* standard checks */ + keys.forEach(function (k) { + if (ndebug) { + out[k] = noop; + return; + } + var type = types[k]; + out[k] = function (arg, msg) { + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* optional checks */ + keys.forEach(function (k) { + var name = 'optional' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* arrayOf checks */ + keys.forEach(function (k) { + var name = 'arrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* optionalArrayOf checks */ + keys.forEach(function (k) { + var name = 'optionalArrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* re-export built-in assertions */ + Object.keys(assert).forEach(function (k) { + if (k === 'AssertionError') { + out[k] = assert[k]; + return; + } + if (ndebug) { + out[k] = noop; + return; + } + out[k] = assert[k]; + }); + + /* export ourselves (for unit tests _only_) */ + out._setExports = _setExports; + + return out; +} + +module.exports = _setExports(process.env.NODE_NDEBUG); + + +/***/ }), +/* 17 */ +/***/ (function(module, exports) { + +// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 +var global = module.exports = typeof window != 'undefined' && window.Math == Math + ? window : typeof self != 'undefined' && self.Math == Math ? self + // eslint-disable-next-line no-new-func + : Function('return this')(); +if (typeof __g == 'number') __g = global; // eslint-disable-line no-undef + + +/***/ }), +/* 18 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.sortAlpha = sortAlpha; +exports.sortOptionsByFlags = sortOptionsByFlags; +exports.entries = entries; +exports.removePrefix = removePrefix; +exports.removeSuffix = removeSuffix; +exports.addSuffix = addSuffix; +exports.hyphenate = hyphenate; +exports.camelCase = camelCase; +exports.compareSortedArrays = compareSortedArrays; +exports.sleep = sleep; +const _camelCase = __webpack_require__(230); + +function sortAlpha(a, b) { + // sort alphabetically in a deterministic way + const shortLen = Math.min(a.length, b.length); + for (let i = 0; i < shortLen; i++) { + const aChar = a.charCodeAt(i); + const bChar = b.charCodeAt(i); + if (aChar !== bChar) { + return aChar - bChar; + } + } + return a.length - b.length; +} + +function sortOptionsByFlags(a, b) { + const aOpt = a.flags.replace(/-/g, ''); + const bOpt = b.flags.replace(/-/g, ''); + return sortAlpha(aOpt, bOpt); +} + +function entries(obj) { + const entries = []; + if (obj) { + for (const key in obj) { + entries.push([key, obj[key]]); + } + } + return entries; +} + +function removePrefix(pattern, prefix) { + if (pattern.startsWith(prefix)) { + pattern = pattern.slice(prefix.length); + } + + return pattern; +} + +function removeSuffix(pattern, suffix) { + if (pattern.endsWith(suffix)) { + return pattern.slice(0, -suffix.length); + } + + return pattern; +} + +function addSuffix(pattern, suffix) { + if (!pattern.endsWith(suffix)) { + return pattern + suffix; + } + + return pattern; +} + +function hyphenate(str) { + return str.replace(/[A-Z]/g, match => { + return '-' + match.charAt(0).toLowerCase(); + }); +} + +function camelCase(str) { + if (/[A-Z]/.test(str)) { + return null; + } else { + return _camelCase(str); + } +} + +function compareSortedArrays(array1, array2) { + if (array1.length !== array2.length) { + return false; + } + for (let i = 0, len = array1.length; i < len; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; +} + +function sleep(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +/***/ }), +/* 19 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.stringify = exports.parse = undefined; + +var _asyncToGenerator2; + +function _load_asyncToGenerator() { + return _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(2)); +} + +var _parse; + +function _load_parse() { + return _parse = __webpack_require__(105); +} + +Object.defineProperty(exports, 'parse', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_parse || _load_parse()).default; + } +}); + +var _stringify; + +function _load_stringify() { + return _stringify = __webpack_require__(199); +} + +Object.defineProperty(exports, 'stringify', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_stringify || _load_stringify()).default; + } +}); +exports.implodeEntry = implodeEntry; +exports.explodeEntry = explodeEntry; + +var _misc; + +function _load_misc() { + return _misc = __webpack_require__(18); +} + +var _normalizePattern; + +function _load_normalizePattern() { + return _normalizePattern = __webpack_require__(37); +} + +var _parse2; + +function _load_parse2() { + return _parse2 = _interopRequireDefault(__webpack_require__(105)); +} + +var _constants; + +function _load_constants() { + return _constants = __webpack_require__(8); +} + +var _fs; + +function _load_fs() { + return _fs = _interopRequireWildcard(__webpack_require__(4)); +} + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const invariant = __webpack_require__(9); + +const path = __webpack_require__(0); +const ssri = __webpack_require__(77); + +function getName(pattern) { + return (0, (_normalizePattern || _load_normalizePattern()).normalizePattern)(pattern).name; +} + +function blankObjectUndefined(obj) { + return obj && Object.keys(obj).length ? obj : undefined; +} + +function keyForRemote(remote) { + return remote.resolved || (remote.reference && remote.hash ? `${remote.reference}#${remote.hash}` : null); +} + +function serializeIntegrity(integrity) { + // We need this because `Integrity.toString()` does not use sorting to ensure a stable string output + // See https://git.io/vx2Hy + return integrity.toString().split(' ').sort().join(' '); +} + +function implodeEntry(pattern, obj) { + const inferredName = getName(pattern); + const integrity = obj.integrity ? serializeIntegrity(obj.integrity) : ''; + const imploded = { + name: inferredName === obj.name ? undefined : obj.name, + version: obj.version, + uid: obj.uid === obj.version ? undefined : obj.uid, + resolved: obj.resolved, + registry: obj.registry === 'npm' ? undefined : obj.registry, + dependencies: blankObjectUndefined(obj.dependencies), + optionalDependencies: blankObjectUndefined(obj.optionalDependencies), + permissions: blankObjectUndefined(obj.permissions), + prebuiltVariants: blankObjectUndefined(obj.prebuiltVariants) + }; + if (integrity) { + imploded.integrity = integrity; + } + return imploded; +} + +function explodeEntry(pattern, obj) { + obj.optionalDependencies = obj.optionalDependencies || {}; + obj.dependencies = obj.dependencies || {}; + obj.uid = obj.uid || obj.version; + obj.permissions = obj.permissions || {}; + obj.registry = obj.registry || 'npm'; + obj.name = obj.name || getName(pattern); + const integrity = obj.integrity; + if (integrity && integrity.isIntegrity) { + obj.integrity = ssri.parse(integrity); + } + return obj; +} + +class Lockfile { + constructor({ cache, source, parseResultType } = {}) { + this.source = source || ''; + this.cache = cache; + this.parseResultType = parseResultType; + } + + // source string if the `cache` was parsed + + + // if true, we're parsing an old yarn file and need to update integrity fields + hasEntriesExistWithoutIntegrity() { + if (!this.cache) { + return false; + } + + for (const key in this.cache) { + // $FlowFixMe - `this.cache` is clearly defined at this point + if (!/^.*@(file:|http)/.test(key) && this.cache[key] && !this.cache[key].integrity) { + return true; + } + } + + return false; + } + + static fromDirectory(dir, reporter) { + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // read the manifest in this directory + const lockfileLoc = path.join(dir, (_constants || _load_constants()).LOCKFILE_FILENAME); + + let lockfile; + let rawLockfile = ''; + let parseResult; + + if (yield (_fs || _load_fs()).exists(lockfileLoc)) { + rawLockfile = yield (_fs || _load_fs()).readFile(lockfileLoc); + parseResult = (0, (_parse2 || _load_parse2()).default)(rawLockfile, lockfileLoc); + + if (reporter) { + if (parseResult.type === 'merge') { + reporter.info(reporter.lang('lockfileMerged')); + } else if (parseResult.type === 'conflict') { + reporter.warn(reporter.lang('lockfileConflict')); + } + } + + lockfile = parseResult.object; + } else if (reporter) { + reporter.info(reporter.lang('noLockfileFound')); + } + + return new Lockfile({ cache: lockfile, source: rawLockfile, parseResultType: parseResult && parseResult.type }); + })(); + } + + getLocked(pattern) { + const cache = this.cache; + if (!cache) { + return undefined; + } + + const shrunk = pattern in cache && cache[pattern]; + + if (typeof shrunk === 'string') { + return this.getLocked(shrunk); + } else if (shrunk) { + explodeEntry(pattern, shrunk); + return shrunk; + } + + return undefined; + } + + removePattern(pattern) { + const cache = this.cache; + if (!cache) { + return; + } + delete cache[pattern]; + } + + getLockfile(patterns) { + const lockfile = {}; + const seen = new Map(); + + // order by name so that lockfile manifest is assigned to the first dependency with this manifest + // the others that have the same remoteKey will just refer to the first + // ordering allows for consistency in lockfile when it is serialized + const sortedPatternsKeys = Object.keys(patterns).sort((_misc || _load_misc()).sortAlpha); + + for (var _iterator = sortedPatternsKeys, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref = _i.value; + } + + const pattern = _ref; + + const pkg = patterns[pattern]; + const remote = pkg._remote, + ref = pkg._reference; + + invariant(ref, 'Package is missing a reference'); + invariant(remote, 'Package is missing a remote'); + + const remoteKey = keyForRemote(remote); + const seenPattern = remoteKey && seen.get(remoteKey); + if (seenPattern) { + // no point in duplicating it + lockfile[pattern] = seenPattern; + + // if we're relying on our name being inferred and two of the patterns have + // different inferred names then we need to set it + if (!seenPattern.name && getName(pattern) !== pkg.name) { + seenPattern.name = pkg.name; + } + continue; + } + const obj = implodeEntry(pattern, { + name: pkg.name, + version: pkg.version, + uid: pkg._uid, + resolved: remote.resolved, + integrity: remote.integrity, + registry: remote.registry, + dependencies: pkg.dependencies, + peerDependencies: pkg.peerDependencies, + optionalDependencies: pkg.optionalDependencies, + permissions: ref.permissions, + prebuiltVariants: pkg.prebuiltVariants + }); + + lockfile[pattern] = obj; + + if (remoteKey) { + seen.set(remoteKey, obj); + } + } + + return lockfile; + } +} +exports.default = Lockfile; + +/***/ }), +/* 20 */ +/***/ (function(module, exports, __webpack_require__) { + +var store = __webpack_require__(133)('wks'); +var uid = __webpack_require__(137); +var Symbol = __webpack_require__(17).Symbol; +var USE_SYMBOL = typeof Symbol == 'function'; + +var $exports = module.exports = function (name) { + return store[name] || (store[name] = + USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)('Symbol.' + name)); +}; + +$exports.store = store; + + +/***/ }), +/* 21 */ +/***/ (function(module, exports) { + +exports = module.exports = SemVer; + +// The debug function is excluded entirely from the minified version. +/* nomin */ var debug; +/* nomin */ if (typeof process === 'object' && + /* nomin */ process.env && + /* nomin */ process.env.NODE_DEBUG && + /* nomin */ /\bsemver\b/i.test(process.env.NODE_DEBUG)) + /* nomin */ debug = function() { + /* nomin */ var args = Array.prototype.slice.call(arguments, 0); + /* nomin */ args.unshift('SEMVER'); + /* nomin */ console.log.apply(console, args); + /* nomin */ }; +/* nomin */ else + /* nomin */ debug = function() {}; + +// Note: this is the semver.org version of the spec that it implements +// Not necessarily the package version of this code. +exports.SEMVER_SPEC_VERSION = '2.0.0'; + +var MAX_LENGTH = 256; +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; + +// Max safe segment length for coercion. +var MAX_SAFE_COMPONENT_LENGTH = 16; + +// The actual regexps go on exports.re +var re = exports.re = []; +var src = exports.src = []; +var R = 0; + +// The following Regular Expressions can be used for tokenizing, +// validating, and parsing SemVer version strings. + +// ## Numeric Identifier +// A single `0`, or a non-zero digit followed by zero or more digits. + +var NUMERICIDENTIFIER = R++; +src[NUMERICIDENTIFIER] = '0|[1-9]\\d*'; +var NUMERICIDENTIFIERLOOSE = R++; +src[NUMERICIDENTIFIERLOOSE] = '[0-9]+'; + + +// ## Non-numeric Identifier +// Zero or more digits, followed by a letter or hyphen, and then zero or +// more letters, digits, or hyphens. + +var NONNUMERICIDENTIFIER = R++; +src[NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*'; + + +// ## Main Version +// Three dot-separated numeric identifiers. + +var MAINVERSION = R++; +src[MAINVERSION] = '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')'; + +var MAINVERSIONLOOSE = R++; +src[MAINVERSIONLOOSE] = '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')'; + +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. + +var PRERELEASEIDENTIFIER = R++; +src[PRERELEASEIDENTIFIER] = '(?:' + src[NUMERICIDENTIFIER] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + +var PRERELEASEIDENTIFIERLOOSE = R++; +src[PRERELEASEIDENTIFIERLOOSE] = '(?:' + src[NUMERICIDENTIFIERLOOSE] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + + +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version +// identifiers. + +var PRERELEASE = R++; +src[PRERELEASE] = '(?:-(' + src[PRERELEASEIDENTIFIER] + + '(?:\\.' + src[PRERELEASEIDENTIFIER] + ')*))'; + +var PRERELEASELOOSE = R++; +src[PRERELEASELOOSE] = '(?:-?(' + src[PRERELEASEIDENTIFIERLOOSE] + + '(?:\\.' + src[PRERELEASEIDENTIFIERLOOSE] + ')*))'; + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. + +var BUILDIDENTIFIER = R++; +src[BUILDIDENTIFIER] = '[0-9A-Za-z-]+'; + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata +// identifiers. + +var BUILD = R++; +src[BUILD] = '(?:\\+(' + src[BUILDIDENTIFIER] + + '(?:\\.' + src[BUILDIDENTIFIER] + ')*))'; + + +// ## Full Version String +// A main version, followed optionally by a pre-release version and +// build metadata. + +// Note that the only major, minor, patch, and pre-release sections of +// the version string are capturing groups. The build metadata is not a +// capturing group, because it should not ever be used in version +// comparison. + +var FULL = R++; +var FULLPLAIN = 'v?' + src[MAINVERSION] + + src[PRERELEASE] + '?' + + src[BUILD] + '?'; + +src[FULL] = '^' + FULLPLAIN + '$'; + +// like full, but allows v1.2.3 and =1.2.3, which people do sometimes. +// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty +// common in the npm registry. +var LOOSEPLAIN = '[v=\\s]*' + src[MAINVERSIONLOOSE] + + src[PRERELEASELOOSE] + '?' + + src[BUILD] + '?'; + +var LOOSE = R++; +src[LOOSE] = '^' + LOOSEPLAIN + '$'; + +var GTLT = R++; +src[GTLT] = '((?:<|>)?=?)'; + +// Something like "2.*" or "1.2.x". +// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Only the first item is strictly required. +var XRANGEIDENTIFIERLOOSE = R++; +src[XRANGEIDENTIFIERLOOSE] = src[NUMERICIDENTIFIERLOOSE] + '|x|X|\\*'; +var XRANGEIDENTIFIER = R++; +src[XRANGEIDENTIFIER] = src[NUMERICIDENTIFIER] + '|x|X|\\*'; + +var XRANGEPLAIN = R++; +src[XRANGEPLAIN] = '[v=\\s]*(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:' + src[PRERELEASE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGEPLAINLOOSE = R++; +src[XRANGEPLAINLOOSE] = '[v=\\s]*(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:' + src[PRERELEASELOOSE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGE = R++; +src[XRANGE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAIN] + '$'; +var XRANGELOOSE = R++; +src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'; + +// Coercion. +// Extract anything that could conceivably be a part of a valid semver +var COERCE = R++; +src[COERCE] = '(?:^|[^\\d])' + + '(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:$|[^\\d])'; + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +var LONETILDE = R++; +src[LONETILDE] = '(?:~>?)'; + +var TILDETRIM = R++; +src[TILDETRIM] = '(\\s*)' + src[LONETILDE] + '\\s+'; +re[TILDETRIM] = new RegExp(src[TILDETRIM], 'g'); +var tildeTrimReplace = '$1~'; + +var TILDE = R++; +src[TILDE] = '^' + src[LONETILDE] + src[XRANGEPLAIN] + '$'; +var TILDELOOSE = R++; +src[TILDELOOSE] = '^' + src[LONETILDE] + src[XRANGEPLAINLOOSE] + '$'; + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +var LONECARET = R++; +src[LONECARET] = '(?:\\^)'; + +var CARETTRIM = R++; +src[CARETTRIM] = '(\\s*)' + src[LONECARET] + '\\s+'; +re[CARETTRIM] = new RegExp(src[CARETTRIM], 'g'); +var caretTrimReplace = '$1^'; + +var CARET = R++; +src[CARET] = '^' + src[LONECARET] + src[XRANGEPLAIN] + '$'; +var CARETLOOSE = R++; +src[CARETLOOSE] = '^' + src[LONECARET] + src[XRANGEPLAINLOOSE] + '$'; + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +var COMPARATORLOOSE = R++; +src[COMPARATORLOOSE] = '^' + src[GTLT] + '\\s*(' + LOOSEPLAIN + ')$|^$'; +var COMPARATOR = R++; +src[COMPARATOR] = '^' + src[GTLT] + '\\s*(' + FULLPLAIN + ')$|^$'; + + +// An expression to strip any whitespace between the gtlt and the thing +// it modifies, so that `> 1.2.3` ==> `>1.2.3` +var COMPARATORTRIM = R++; +src[COMPARATORTRIM] = '(\\s*)' + src[GTLT] + + '\\s*(' + LOOSEPLAIN + '|' + src[XRANGEPLAIN] + ')'; + +// this one has to use the /g flag +re[COMPARATORTRIM] = new RegExp(src[COMPARATORTRIM], 'g'); +var comparatorTrimReplace = '$1$2$3'; + + +// Something like `1.2.3 - 1.2.4` +// Note that these all use the loose form, because they'll be +// checked against either the strict or loose comparator form +// later. +var HYPHENRANGE = R++; +src[HYPHENRANGE] = '^\\s*(' + src[XRANGEPLAIN] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAIN] + ')' + + '\\s*$'; + +var HYPHENRANGELOOSE = R++; +src[HYPHENRANGELOOSE] = '^\\s*(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s*$'; + +// Star ranges basically just allow anything at all. +var STAR = R++; +src[STAR] = '(<|>)?=?\\s*\\*'; + +// Compile to actual regexp objects. +// All are flag-free, unless they were created above with a flag. +for (var i = 0; i < R; i++) { + debug(i, src[i]); + if (!re[i]) + re[i] = new RegExp(src[i]); +} + +exports.parse = parse; +function parse(version, loose) { + if (version instanceof SemVer) + return version; + + if (typeof version !== 'string') + return null; + + if (version.length > MAX_LENGTH) + return null; + + var r = loose ? re[LOOSE] : re[FULL]; + if (!r.test(version)) + return null; + + try { + return new SemVer(version, loose); + } catch (er) { + return null; + } +} + +exports.valid = valid; +function valid(version, loose) { + var v = parse(version, loose); + return v ? v.version : null; +} + + +exports.clean = clean; +function clean(version, loose) { + var s = parse(version.trim().replace(/^[=v]+/, ''), loose); + return s ? s.version : null; +} + +exports.SemVer = SemVer; + +function SemVer(version, loose) { + if (version instanceof SemVer) { + if (version.loose === loose) + return version; + else + version = version.version; + } else if (typeof version !== 'string') { + throw new TypeError('Invalid Version: ' + version); + } + + if (version.length > MAX_LENGTH) + throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') + + if (!(this instanceof SemVer)) + return new SemVer(version, loose); + + debug('SemVer', version, loose); + this.loose = loose; + var m = version.trim().match(loose ? re[LOOSE] : re[FULL]); + + if (!m) + throw new TypeError('Invalid Version: ' + version); + + this.raw = version; + + // these are actually numbers + this.major = +m[1]; + this.minor = +m[2]; + this.patch = +m[3]; + + if (this.major > MAX_SAFE_INTEGER || this.major < 0) + throw new TypeError('Invalid major version') + + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) + throw new TypeError('Invalid minor version') + + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) + throw new TypeError('Invalid patch version') + + // numberify any prerelease numeric ids + if (!m[4]) + this.prerelease = []; + else + this.prerelease = m[4].split('.').map(function(id) { + if (/^[0-9]+$/.test(id)) { + var num = +id; + if (num >= 0 && num < MAX_SAFE_INTEGER) + return num; + } + return id; + }); + + this.build = m[5] ? m[5].split('.') : []; + this.format(); +} + +SemVer.prototype.format = function() { + this.version = this.major + '.' + this.minor + '.' + this.patch; + if (this.prerelease.length) + this.version += '-' + this.prerelease.join('.'); + return this.version; +}; + +SemVer.prototype.toString = function() { + return this.version; +}; + +SemVer.prototype.compare = function(other) { + debug('SemVer.compare', this.version, this.loose, other); + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return this.compareMain(other) || this.comparePre(other); +}; + +SemVer.prototype.compareMain = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return compareIdentifiers(this.major, other.major) || + compareIdentifiers(this.minor, other.minor) || + compareIdentifiers(this.patch, other.patch); +}; + +SemVer.prototype.comparePre = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + // NOT having a prerelease is > having one + if (this.prerelease.length && !other.prerelease.length) + return -1; + else if (!this.prerelease.length && other.prerelease.length) + return 1; + else if (!this.prerelease.length && !other.prerelease.length) + return 0; + + var i = 0; + do { + var a = this.prerelease[i]; + var b = other.prerelease[i]; + debug('prerelease compare', i, a, b); + if (a === undefined && b === undefined) + return 0; + else if (b === undefined) + return 1; + else if (a === undefined) + return -1; + else if (a === b) + continue; + else + return compareIdentifiers(a, b); + } while (++i); +}; + +// preminor will bump the version up to the next minor release, and immediately +// down to pre-release. premajor and prepatch work the same way. +SemVer.prototype.inc = function(release, identifier) { + switch (release) { + case 'premajor': + this.prerelease.length = 0; + this.patch = 0; + this.minor = 0; + this.major++; + this.inc('pre', identifier); + break; + case 'preminor': + this.prerelease.length = 0; + this.patch = 0; + this.minor++; + this.inc('pre', identifier); + break; + case 'prepatch': + // If this is already a prerelease, it will bump to the next version + // drop any prereleases that might already exist, since they are not + // relevant at this point. + this.prerelease.length = 0; + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + // If the input is a non-prerelease version, this acts the same as + // prepatch. + case 'prerelease': + if (this.prerelease.length === 0) + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + + case 'major': + // If this is a pre-major version, bump up to the same major version. + // Otherwise increment major. + // 1.0.0-5 bumps to 1.0.0 + // 1.1.0 bumps to 2.0.0 + if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) + this.major++; + this.minor = 0; + this.patch = 0; + this.prerelease = []; + break; + case 'minor': + // If this is a pre-minor version, bump up to the same minor version. + // Otherwise increment minor. + // 1.2.0-5 bumps to 1.2.0 + // 1.2.1 bumps to 1.3.0 + if (this.patch !== 0 || this.prerelease.length === 0) + this.minor++; + this.patch = 0; + this.prerelease = []; + break; + case 'patch': + // If this is not a pre-release version, it will increment the patch. + // If it is a pre-release it will bump up to the same patch version. + // 1.2.0-5 patches to 1.2.0 + // 1.2.0 patches to 1.2.1 + if (this.prerelease.length === 0) + this.patch++; + this.prerelease = []; + break; + // This probably shouldn't be used publicly. + // 1.0.0 "pre" would become 1.0.0-0 which is the wrong direction. + case 'pre': + if (this.prerelease.length === 0) + this.prerelease = [0]; + else { + var i = this.prerelease.length; + while (--i >= 0) { + if (typeof this.prerelease[i] === 'number') { + this.prerelease[i]++; + i = -2; + } + } + if (i === -1) // didn't increment anything + this.prerelease.push(0); + } + if (identifier) { + // 1.2.0-beta.1 bumps to 1.2.0-beta.2, + // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + if (this.prerelease[0] === identifier) { + if (isNaN(this.prerelease[1])) + this.prerelease = [identifier, 0]; + } else + this.prerelease = [identifier, 0]; + } + break; + + default: + throw new Error('invalid increment argument: ' + release); + } + this.format(); + this.raw = this.version; + return this; +}; + +exports.inc = inc; +function inc(version, release, loose, identifier) { + if (typeof(loose) === 'string') { + identifier = loose; + loose = undefined; + } + + try { + return new SemVer(version, loose).inc(release, identifier).version; + } catch (er) { + return null; + } +} + +exports.diff = diff; +function diff(version1, version2) { + if (eq(version1, version2)) { + return null; + } else { + var v1 = parse(version1); + var v2 = parse(version2); + if (v1.prerelease.length || v2.prerelease.length) { + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return 'pre'+key; + } + } + } + return 'prerelease'; + } + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return key; + } + } + } + } +} + +exports.compareIdentifiers = compareIdentifiers; + +var numeric = /^[0-9]+$/; +function compareIdentifiers(a, b) { + var anum = numeric.test(a); + var bnum = numeric.test(b); + + if (anum && bnum) { + a = +a; + b = +b; + } + + return (anum && !bnum) ? -1 : + (bnum && !anum) ? 1 : + a < b ? -1 : + a > b ? 1 : + 0; +} + +exports.rcompareIdentifiers = rcompareIdentifiers; +function rcompareIdentifiers(a, b) { + return compareIdentifiers(b, a); +} + +exports.major = major; +function major(a, loose) { + return new SemVer(a, loose).major; +} + +exports.minor = minor; +function minor(a, loose) { + return new SemVer(a, loose).minor; +} + +exports.patch = patch; +function patch(a, loose) { + return new SemVer(a, loose).patch; +} + +exports.compare = compare; +function compare(a, b, loose) { + return new SemVer(a, loose).compare(new SemVer(b, loose)); +} + +exports.compareLoose = compareLoose; +function compareLoose(a, b) { + return compare(a, b, true); +} + +exports.rcompare = rcompare; +function rcompare(a, b, loose) { + return compare(b, a, loose); +} + +exports.sort = sort; +function sort(list, loose) { + return list.sort(function(a, b) { + return exports.compare(a, b, loose); + }); +} + +exports.rsort = rsort; +function rsort(list, loose) { + return list.sort(function(a, b) { + return exports.rcompare(a, b, loose); + }); +} + +exports.gt = gt; +function gt(a, b, loose) { + return compare(a, b, loose) > 0; +} + +exports.lt = lt; +function lt(a, b, loose) { + return compare(a, b, loose) < 0; +} + +exports.eq = eq; +function eq(a, b, loose) { + return compare(a, b, loose) === 0; +} + +exports.neq = neq; +function neq(a, b, loose) { + return compare(a, b, loose) !== 0; +} + +exports.gte = gte; +function gte(a, b, loose) { + return compare(a, b, loose) >= 0; +} + +exports.lte = lte; +function lte(a, b, loose) { + return compare(a, b, loose) <= 0; +} + +exports.cmp = cmp; +function cmp(a, op, b, loose) { + var ret; + switch (op) { + case '===': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a === b; + break; + case '!==': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a !== b; + break; + case '': case '=': case '==': ret = eq(a, b, loose); break; + case '!=': ret = neq(a, b, loose); break; + case '>': ret = gt(a, b, loose); break; + case '>=': ret = gte(a, b, loose); break; + case '<': ret = lt(a, b, loose); break; + case '<=': ret = lte(a, b, loose); break; + default: throw new TypeError('Invalid operator: ' + op); + } + return ret; +} + +exports.Comparator = Comparator; +function Comparator(comp, loose) { + if (comp instanceof Comparator) { + if (comp.loose === loose) + return comp; + else + comp = comp.value; + } + + if (!(this instanceof Comparator)) + return new Comparator(comp, loose); + + debug('comparator', comp, loose); + this.loose = loose; + this.parse(comp); + + if (this.semver === ANY) + this.value = ''; + else + this.value = this.operator + this.semver.version; + + debug('comp', this); +} + +var ANY = {}; +Comparator.prototype.parse = function(comp) { + var r = this.loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var m = comp.match(r); + + if (!m) + throw new TypeError('Invalid comparator: ' + comp); + + this.operator = m[1]; + if (this.operator === '=') + this.operator = ''; + + // if it literally is just '>' or '' then allow anything. + if (!m[2]) + this.semver = ANY; + else + this.semver = new SemVer(m[2], this.loose); +}; + +Comparator.prototype.toString = function() { + return this.value; +}; + +Comparator.prototype.test = function(version) { + debug('Comparator.test', version, this.loose); + + if (this.semver === ANY) + return true; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + return cmp(version, this.operator, this.semver, this.loose); +}; + +Comparator.prototype.intersects = function(comp, loose) { + if (!(comp instanceof Comparator)) { + throw new TypeError('a Comparator is required'); + } + + var rangeTmp; + + if (this.operator === '') { + rangeTmp = new Range(comp.value, loose); + return satisfies(this.value, rangeTmp, loose); + } else if (comp.operator === '') { + rangeTmp = new Range(this.value, loose); + return satisfies(comp.semver, rangeTmp, loose); + } + + var sameDirectionIncreasing = + (this.operator === '>=' || this.operator === '>') && + (comp.operator === '>=' || comp.operator === '>'); + var sameDirectionDecreasing = + (this.operator === '<=' || this.operator === '<') && + (comp.operator === '<=' || comp.operator === '<'); + var sameSemVer = this.semver.version === comp.semver.version; + var differentDirectionsInclusive = + (this.operator === '>=' || this.operator === '<=') && + (comp.operator === '>=' || comp.operator === '<='); + var oppositeDirectionsLessThan = + cmp(this.semver, '<', comp.semver, loose) && + ((this.operator === '>=' || this.operator === '>') && + (comp.operator === '<=' || comp.operator === '<')); + var oppositeDirectionsGreaterThan = + cmp(this.semver, '>', comp.semver, loose) && + ((this.operator === '<=' || this.operator === '<') && + (comp.operator === '>=' || comp.operator === '>')); + + return sameDirectionIncreasing || sameDirectionDecreasing || + (sameSemVer && differentDirectionsInclusive) || + oppositeDirectionsLessThan || oppositeDirectionsGreaterThan; +}; + + +exports.Range = Range; +function Range(range, loose) { + if (range instanceof Range) { + if (range.loose === loose) { + return range; + } else { + return new Range(range.raw, loose); + } + } + + if (range instanceof Comparator) { + return new Range(range.value, loose); + } + + if (!(this instanceof Range)) + return new Range(range, loose); + + this.loose = loose; + + // First, split based on boolean or || + this.raw = range; + this.set = range.split(/\s*\|\|\s*/).map(function(range) { + return this.parseRange(range.trim()); + }, this).filter(function(c) { + // throw out any that are not relevant for whatever reason + return c.length; + }); + + if (!this.set.length) { + throw new TypeError('Invalid SemVer Range: ' + range); + } + + this.format(); +} + +Range.prototype.format = function() { + this.range = this.set.map(function(comps) { + return comps.join(' ').trim(); + }).join('||').trim(); + return this.range; +}; + +Range.prototype.toString = function() { + return this.range; +}; + +Range.prototype.parseRange = function(range) { + var loose = this.loose; + range = range.trim(); + debug('range', range, loose); + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + var hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE]; + range = range.replace(hr, hyphenReplace); + debug('hyphen replace', range); + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace); + debug('comparator trim', range, re[COMPARATORTRIM]); + + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[TILDETRIM], tildeTrimReplace); + + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[CARETTRIM], caretTrimReplace); + + // normalize spaces + range = range.split(/\s+/).join(' '); + + // At this point, the range is completely trimmed and + // ready to be split into comparators. + + var compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var set = range.split(' ').map(function(comp) { + return parseComparator(comp, loose); + }).join(' ').split(/\s+/); + if (this.loose) { + // in loose mode, throw out any that are not valid comparators + set = set.filter(function(comp) { + return !!comp.match(compRe); + }); + } + set = set.map(function(comp) { + return new Comparator(comp, loose); + }); + + return set; +}; + +Range.prototype.intersects = function(range, loose) { + if (!(range instanceof Range)) { + throw new TypeError('a Range is required'); + } + + return this.set.some(function(thisComparators) { + return thisComparators.every(function(thisComparator) { + return range.set.some(function(rangeComparators) { + return rangeComparators.every(function(rangeComparator) { + return thisComparator.intersects(rangeComparator, loose); + }); + }); + }); + }); +}; + +// Mostly just for testing and legacy API reasons +exports.toComparators = toComparators; +function toComparators(range, loose) { + return new Range(range, loose).set.map(function(comp) { + return comp.map(function(c) { + return c.value; + }).join(' ').trim().split(' '); + }); +} + +// comprised of xranges, tildes, stars, and gtlt's at this point. +// already replaced the hyphen ranges +// turn into a set of JUST comparators. +function parseComparator(comp, loose) { + debug('comp', comp); + comp = replaceCarets(comp, loose); + debug('caret', comp); + comp = replaceTildes(comp, loose); + debug('tildes', comp); + comp = replaceXRanges(comp, loose); + debug('xrange', comp); + comp = replaceStars(comp, loose); + debug('stars', comp); + return comp; +} + +function isX(id) { + return !id || id.toLowerCase() === 'x' || id === '*'; +} + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 +function replaceTildes(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceTilde(comp, loose); + }).join(' '); +} + +function replaceTilde(comp, loose) { + var r = loose ? re[TILDELOOSE] : re[TILDE]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('tilde', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) + // ~1.2 == >=1.2.0 <1.3.0 + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else if (pr) { + debug('replaceTilde pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + // ~1.2.3 == >=1.2.3 <1.3.0 + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + + debug('tilde return', ret); + return ret; + }); +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2.0 --> >=1.2.0 <2.0.0 +function replaceCarets(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceCaret(comp, loose); + }).join(' '); +} + +function replaceCaret(comp, loose) { + debug('caret', comp, loose); + var r = loose ? re[CARETLOOSE] : re[CARET]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('caret', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) { + if (M === '0') + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else + ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0'; + } else if (pr) { + debug('replaceCaret pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + (+M + 1) + '.0.0'; + } else { + debug('no pr'); + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + (+M + 1) + '.0.0'; + } + + debug('caret return', ret); + return ret; + }); +} + +function replaceXRanges(comp, loose) { + debug('replaceXRanges', comp, loose); + return comp.split(/\s+/).map(function(comp) { + return replaceXRange(comp, loose); + }).join(' '); +} + +function replaceXRange(comp, loose) { + comp = comp.trim(); + var r = loose ? re[XRANGELOOSE] : re[XRANGE]; + return comp.replace(r, function(ret, gtlt, M, m, p, pr) { + debug('xRange', comp, ret, gtlt, M, m, p, pr); + var xM = isX(M); + var xm = xM || isX(m); + var xp = xm || isX(p); + var anyX = xp; + + if (gtlt === '=' && anyX) + gtlt = ''; + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0'; + } else { + // nothing is forbidden + ret = '*'; + } + } else if (gtlt && anyX) { + // replace X with 0 + if (xm) + m = 0; + if (xp) + p = 0; + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + // >1.2.3 => >= 1.2.4 + gtlt = '>='; + if (xm) { + M = +M + 1; + m = 0; + p = 0; + } else if (xp) { + m = +m + 1; + p = 0; + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<'; + if (xm) + M = +M + 1; + else + m = +m + 1; + } + + ret = gtlt + M + '.' + m + '.' + p; + } else if (xm) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + } else if (xp) { + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + } + + debug('xRange return', ret); + + return ret; + }); +} + +// Because * is AND-ed with everything else in the comparator, +// and '' means "any version", just remove the *s entirely. +function replaceStars(comp, loose) { + debug('replaceStars', comp, loose); + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[STAR], ''); +} + +// This function is passed to string.replace(re[HYPHENRANGE]) +// M, m, patch, prerelease, build +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0 +function hyphenReplace($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr, tb) { + + if (isX(fM)) + from = ''; + else if (isX(fm)) + from = '>=' + fM + '.0.0'; + else if (isX(fp)) + from = '>=' + fM + '.' + fm + '.0'; + else + from = '>=' + from; + + if (isX(tM)) + to = ''; + else if (isX(tm)) + to = '<' + (+tM + 1) + '.0.0'; + else if (isX(tp)) + to = '<' + tM + '.' + (+tm + 1) + '.0'; + else if (tpr) + to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr; + else + to = '<=' + to; + + return (from + ' ' + to).trim(); +} + + +// if ANY of the sets match ALL of its comparators, then pass +Range.prototype.test = function(version) { + if (!version) + return false; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + for (var i = 0; i < this.set.length; i++) { + if (testSet(this.set[i], version)) + return true; + } + return false; +}; + +function testSet(set, version) { + for (var i = 0; i < set.length; i++) { + if (!set[i].test(version)) + return false; + } + + if (version.prerelease.length) { + // Find the set of versions that are allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, + // even though it's within the range set by the comparators. + for (var i = 0; i < set.length; i++) { + debug(set[i].semver); + if (set[i].semver === ANY) + continue; + + if (set[i].semver.prerelease.length > 0) { + var allowed = set[i].semver; + if (allowed.major === version.major && + allowed.minor === version.minor && + allowed.patch === version.patch) + return true; + } + } + + // Version has a -pre, but it's not one of the ones we like. + return false; + } + + return true; +} + +exports.satisfies = satisfies; +function satisfies(version, range, loose) { + try { + range = new Range(range, loose); + } catch (er) { + return false; + } + return range.test(version); +} + +exports.maxSatisfying = maxSatisfying; +function maxSatisfying(versions, range, loose) { + var max = null; + var maxSV = null; + try { + var rangeObj = new Range(range, loose); + } catch (er) { + return null; + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { // satisfies(v, range, loose) + if (!max || maxSV.compare(v) === -1) { // compare(max, v, true) + max = v; + maxSV = new SemVer(max, loose); + } + } + }) + return max; +} + +exports.minSatisfying = minSatisfying; +function minSatisfying(versions, range, loose) { + var min = null; + var minSV = null; + try { + var rangeObj = new Range(range, loose); + } catch (er) { + return null; + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { // satisfies(v, range, loose) + if (!min || minSV.compare(v) === 1) { // compare(min, v, true) + min = v; + minSV = new SemVer(min, loose); + } + } + }) + return min; +} + +exports.validRange = validRange; +function validRange(range, loose) { + try { + // Return '*' instead of '' so that truthiness works. + // This will throw if it's invalid anyway + return new Range(range, loose).range || '*'; + } catch (er) { + return null; + } +} + +// Determine if version is less than all the versions possible in the range +exports.ltr = ltr; +function ltr(version, range, loose) { + return outside(version, range, '<', loose); +} + +// Determine if version is greater than all the versions possible in the range. +exports.gtr = gtr; +function gtr(version, range, loose) { + return outside(version, range, '>', loose); +} + +exports.outside = outside; +function outside(version, range, hilo, loose) { + version = new SemVer(version, loose); + range = new Range(range, loose); + + var gtfn, ltefn, ltfn, comp, ecomp; + switch (hilo) { + case '>': + gtfn = gt; + ltefn = lte; + ltfn = lt; + comp = '>'; + ecomp = '>='; + break; + case '<': + gtfn = lt; + ltefn = gte; + ltfn = gt; + comp = '<'; + ecomp = '<='; + break; + default: + throw new TypeError('Must provide a hilo val of "<" or ">"'); + } + + // If it satisifes the range it is not outside + if (satisfies(version, range, loose)) { + return false; + } + + // From now on, variable terms are as if we're in "gtr" mode. + // but note that everything is flipped for the "ltr" function. + + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i]; + + var high = null; + var low = null; + + comparators.forEach(function(comparator) { + if (comparator.semver === ANY) { + comparator = new Comparator('>=0.0.0') + } + high = high || comparator; + low = low || comparator; + if (gtfn(comparator.semver, high.semver, loose)) { + high = comparator; + } else if (ltfn(comparator.semver, low.semver, loose)) { + low = comparator; + } + }); + + // If the edge version comparator has a operator then our version + // isn't outside it + if (high.operator === comp || high.operator === ecomp) { + return false; + } + + // If the lowest version comparator has an operator and our version + // is less than it then it isn't higher than the range + if ((!low.operator || low.operator === comp) && + ltefn(version, low.semver)) { + return false; + } else if (low.operator === ecomp && ltfn(version, low.semver)) { + return false; + } + } + return true; +} + +exports.prerelease = prerelease; +function prerelease(version, loose) { + var parsed = parse(version, loose); + return (parsed && parsed.prerelease.length) ? parsed.prerelease : null; +} + +exports.intersects = intersects; +function intersects(r1, r2, loose) { + r1 = new Range(r1, loose) + r2 = new Range(r2, loose) + return r1.intersects(r2) +} + +exports.coerce = coerce; +function coerce(version) { + if (version instanceof SemVer) + return version; + + if (typeof version !== 'string') + return null; + + var match = version.match(re[COERCE]); + + if (match == null) + return null; + + return parse((match[1] || '0') + '.' + (match[2] || '0') + '.' + (match[3] || '0')); +} + + +/***/ }), +/* 22 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +exports.__esModule = true; + +var _assign = __webpack_require__(591); + +var _assign2 = _interopRequireDefault(_assign); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = _assign2.default || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; +}; + +/***/ }), +/* 23 */ +/***/ (function(module, exports) { + +module.exports = require("stream"); + +/***/ }), +/* 24 */ +/***/ (function(module, exports) { + +module.exports = require("url"); + +/***/ }), +/* 25 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Subscription; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util_isArray__ = __webpack_require__(41); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__util_isObject__ = __webpack_require__(444); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__util_isFunction__ = __webpack_require__(154); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__util_tryCatch__ = __webpack_require__(56); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__util_errorObject__ = __webpack_require__(47); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__ = __webpack_require__(441); +/** PURE_IMPORTS_START _util_isArray,_util_isObject,_util_isFunction,_util_tryCatch,_util_errorObject,_util_UnsubscriptionError PURE_IMPORTS_END */ + + + + + + +var Subscription = /*@__PURE__*/ (function () { + function Subscription(unsubscribe) { + this.closed = false; + this._parent = null; + this._parents = null; + this._subscriptions = null; + if (unsubscribe) { + this._unsubscribe = unsubscribe; + } + } + Subscription.prototype.unsubscribe = function () { + var hasErrors = false; + var errors; + if (this.closed) { + return; + } + var _a = this, _parent = _a._parent, _parents = _a._parents, _unsubscribe = _a._unsubscribe, _subscriptions = _a._subscriptions; + this.closed = true; + this._parent = null; + this._parents = null; + this._subscriptions = null; + var index = -1; + var len = _parents ? _parents.length : 0; + while (_parent) { + _parent.remove(this); + _parent = ++index < len && _parents[index] || null; + } + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_2__util_isFunction__["a" /* isFunction */])(_unsubscribe)) { + var trial = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_3__util_tryCatch__["a" /* tryCatch */])(_unsubscribe).call(this); + if (trial === __WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */]) { + hasErrors = true; + errors = errors || (__WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e instanceof __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */] ? + flattenUnsubscriptionErrors(__WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e.errors) : [__WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e]); + } + } + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__util_isArray__["a" /* isArray */])(_subscriptions)) { + index = -1; + len = _subscriptions.length; + while (++index < len) { + var sub = _subscriptions[index]; + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_isObject__["a" /* isObject */])(sub)) { + var trial = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_3__util_tryCatch__["a" /* tryCatch */])(sub.unsubscribe).call(sub); + if (trial === __WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */]) { + hasErrors = true; + errors = errors || []; + var err = __WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e; + if (err instanceof __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */]) { + errors = errors.concat(flattenUnsubscriptionErrors(err.errors)); + } + else { + errors.push(err); + } + } + } + } + } + if (hasErrors) { + throw new __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */](errors); + } + }; + Subscription.prototype.add = function (teardown) { + if (!teardown || (teardown === Subscription.EMPTY)) { + return Subscription.EMPTY; + } + if (teardown === this) { + return this; + } + var subscription = teardown; + switch (typeof teardown) { + case 'function': + subscription = new Subscription(teardown); + case 'object': + if (subscription.closed || typeof subscription.unsubscribe !== 'function') { + return subscription; + } + else if (this.closed) { + subscription.unsubscribe(); + return subscription; + } + else if (typeof subscription._addParent !== 'function') { + var tmp = subscription; + subscription = new Subscription(); + subscription._subscriptions = [tmp]; + } + break; + default: + throw new Error('unrecognized teardown ' + teardown + ' added to Subscription.'); + } + var subscriptions = this._subscriptions || (this._subscriptions = []); + subscriptions.push(subscription); + subscription._addParent(this); + return subscription; + }; + Subscription.prototype.remove = function (subscription) { + var subscriptions = this._subscriptions; + if (subscriptions) { + var subscriptionIndex = subscriptions.indexOf(subscription); + if (subscriptionIndex !== -1) { + subscriptions.splice(subscriptionIndex, 1); + } + } + }; + Subscription.prototype._addParent = function (parent) { + var _a = this, _parent = _a._parent, _parents = _a._parents; + if (!_parent || _parent === parent) { + this._parent = parent; + } + else if (!_parents) { + this._parents = [parent]; + } + else if (_parents.indexOf(parent) === -1) { + _parents.push(parent); + } + }; + Subscription.EMPTY = (function (empty) { + empty.closed = true; + return empty; + }(new Subscription())); + return Subscription; +}()); + +function flattenUnsubscriptionErrors(errors) { + return errors.reduce(function (errs, err) { return errs.concat((err instanceof __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */]) ? err.errors : err); }, []); +} +//# sourceMappingURL=Subscription.js.map + + +/***/ }), +/* 26 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = { + bufferSplit: bufferSplit, + addRSAMissing: addRSAMissing, + calculateDSAPublic: calculateDSAPublic, + calculateED25519Public: calculateED25519Public, + calculateX25519Public: calculateX25519Public, + mpNormalize: mpNormalize, + mpDenormalize: mpDenormalize, + ecNormalize: ecNormalize, + countZeros: countZeros, + assertCompatible: assertCompatible, + isCompatible: isCompatible, + opensslKeyDeriv: opensslKeyDeriv, + opensshCipherInfo: opensshCipherInfo, + publicFromPrivateECDSA: publicFromPrivateECDSA, + zeroPadToLength: zeroPadToLength, + writeBitString: writeBitString, + readBitString: readBitString +}; + +var assert = __webpack_require__(16); +var Buffer = __webpack_require__(15).Buffer; +var PrivateKey = __webpack_require__(33); +var Key = __webpack_require__(27); +var crypto = __webpack_require__(12); +var algs = __webpack_require__(32); +var asn1 = __webpack_require__(65); + +var ec, jsbn; +var nacl; + +var MAX_CLASS_DEPTH = 3; + +function isCompatible(obj, klass, needVer) { + if (obj === null || typeof (obj) !== 'object') + return (false); + if (needVer === undefined) + needVer = klass.prototype._sshpkApiVersion; + if (obj instanceof klass && + klass.prototype._sshpkApiVersion[0] == needVer[0]) + return (true); + var proto = Object.getPrototypeOf(obj); + var depth = 0; + while (proto.constructor.name !== klass.name) { + proto = Object.getPrototypeOf(proto); + if (!proto || ++depth > MAX_CLASS_DEPTH) + return (false); + } + if (proto.constructor.name !== klass.name) + return (false); + var ver = proto._sshpkApiVersion; + if (ver === undefined) + ver = klass._oldVersionDetect(obj); + if (ver[0] != needVer[0] || ver[1] < needVer[1]) + return (false); + return (true); +} + +function assertCompatible(obj, klass, needVer, name) { + if (name === undefined) + name = 'object'; + assert.ok(obj, name + ' must not be null'); + assert.object(obj, name + ' must be an object'); + if (needVer === undefined) + needVer = klass.prototype._sshpkApiVersion; + if (obj instanceof klass && + klass.prototype._sshpkApiVersion[0] == needVer[0]) + return; + var proto = Object.getPrototypeOf(obj); + var depth = 0; + while (proto.constructor.name !== klass.name) { + proto = Object.getPrototypeOf(proto); + assert.ok(proto && ++depth <= MAX_CLASS_DEPTH, + name + ' must be a ' + klass.name + ' instance'); + } + assert.strictEqual(proto.constructor.name, klass.name, + name + ' must be a ' + klass.name + ' instance'); + var ver = proto._sshpkApiVersion; + if (ver === undefined) + ver = klass._oldVersionDetect(obj); + assert.ok(ver[0] == needVer[0] && ver[1] >= needVer[1], + name + ' must be compatible with ' + klass.name + ' klass ' + + 'version ' + needVer[0] + '.' + needVer[1]); +} + +var CIPHER_LEN = { + 'des-ede3-cbc': { key: 7, iv: 8 }, + 'aes-128-cbc': { key: 16, iv: 16 } +}; +var PKCS5_SALT_LEN = 8; + +function opensslKeyDeriv(cipher, salt, passphrase, count) { + assert.buffer(salt, 'salt'); + assert.buffer(passphrase, 'passphrase'); + assert.number(count, 'iteration count'); + + var clen = CIPHER_LEN[cipher]; + assert.object(clen, 'supported cipher'); + + salt = salt.slice(0, PKCS5_SALT_LEN); + + var D, D_prev, bufs; + var material = Buffer.alloc(0); + while (material.length < clen.key + clen.iv) { + bufs = []; + if (D_prev) + bufs.push(D_prev); + bufs.push(passphrase); + bufs.push(salt); + D = Buffer.concat(bufs); + for (var j = 0; j < count; ++j) + D = crypto.createHash('md5').update(D).digest(); + material = Buffer.concat([material, D]); + D_prev = D; + } + + return ({ + key: material.slice(0, clen.key), + iv: material.slice(clen.key, clen.key + clen.iv) + }); +} + +/* Count leading zero bits on a buffer */ +function countZeros(buf) { + var o = 0, obit = 8; + while (o < buf.length) { + var mask = (1 << obit); + if ((buf[o] & mask) === mask) + break; + obit--; + if (obit < 0) { + o++; + obit = 8; + } + } + return (o*8 + (8 - obit) - 1); +} + +function bufferSplit(buf, chr) { + assert.buffer(buf); + assert.string(chr); + + var parts = []; + var lastPart = 0; + var matches = 0; + for (var i = 0; i < buf.length; ++i) { + if (buf[i] === chr.charCodeAt(matches)) + ++matches; + else if (buf[i] === chr.charCodeAt(0)) + matches = 1; + else + matches = 0; + + if (matches >= chr.length) { + var newPart = i + 1; + parts.push(buf.slice(lastPart, newPart - matches)); + lastPart = newPart; + matches = 0; + } + } + if (lastPart <= buf.length) + parts.push(buf.slice(lastPart, buf.length)); + + return (parts); +} + +function ecNormalize(buf, addZero) { + assert.buffer(buf); + if (buf[0] === 0x00 && buf[1] === 0x04) { + if (addZero) + return (buf); + return (buf.slice(1)); + } else if (buf[0] === 0x04) { + if (!addZero) + return (buf); + } else { + while (buf[0] === 0x00) + buf = buf.slice(1); + if (buf[0] === 0x02 || buf[0] === 0x03) + throw (new Error('Compressed elliptic curve points ' + + 'are not supported')); + if (buf[0] !== 0x04) + throw (new Error('Not a valid elliptic curve point')); + if (!addZero) + return (buf); + } + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x0; + buf.copy(b, 1); + return (b); +} + +function readBitString(der, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var buf = der.readString(tag, true); + assert.strictEqual(buf[0], 0x00, 'bit strings with unused bits are ' + + 'not supported (0x' + buf[0].toString(16) + ')'); + return (buf.slice(1)); +} + +function writeBitString(der, buf, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + der.writeBuffer(b, tag); +} + +function mpNormalize(buf) { + assert.buffer(buf); + while (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80) === 0x00) + buf = buf.slice(1); + if ((buf[0] & 0x80) === 0x80) { + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + buf = b; + } + return (buf); +} + +function mpDenormalize(buf) { + assert.buffer(buf); + while (buf.length > 1 && buf[0] === 0x00) + buf = buf.slice(1); + return (buf); +} + +function zeroPadToLength(buf, len) { + assert.buffer(buf); + assert.number(len); + while (buf.length > len) { + assert.equal(buf[0], 0x00); + buf = buf.slice(1); + } + while (buf.length < len) { + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + buf = b; + } + return (buf); +} + +function bigintToMpBuf(bigint) { + var buf = Buffer.from(bigint.toByteArray()); + buf = mpNormalize(buf); + return (buf); +} + +function calculateDSAPublic(g, p, x) { + assert.buffer(g); + assert.buffer(p); + assert.buffer(x); + try { + var bigInt = __webpack_require__(81).BigInteger; + } catch (e) { + throw (new Error('To load a PKCS#8 format DSA private key, ' + + 'the node jsbn library is required.')); + } + g = new bigInt(g); + p = new bigInt(p); + x = new bigInt(x); + var y = g.modPow(x, p); + var ybuf = bigintToMpBuf(y); + return (ybuf); +} + +function calculateED25519Public(k) { + assert.buffer(k); + + if (nacl === undefined) + nacl = __webpack_require__(75); + + var kp = nacl.sign.keyPair.fromSeed(new Uint8Array(k)); + return (Buffer.from(kp.publicKey)); +} + +function calculateX25519Public(k) { + assert.buffer(k); + + if (nacl === undefined) + nacl = __webpack_require__(75); + + var kp = nacl.box.keyPair.fromSeed(new Uint8Array(k)); + return (Buffer.from(kp.publicKey)); +} + +function addRSAMissing(key) { + assert.object(key); + assertCompatible(key, PrivateKey, [1, 1]); + try { + var bigInt = __webpack_require__(81).BigInteger; + } catch (e) { + throw (new Error('To write a PEM private key from ' + + 'this source, the node jsbn lib is required.')); + } + + var d = new bigInt(key.part.d.data); + var buf; + + if (!key.part.dmodp) { + var p = new bigInt(key.part.p.data); + var dmodp = d.mod(p.subtract(1)); + + buf = bigintToMpBuf(dmodp); + key.part.dmodp = {name: 'dmodp', data: buf}; + key.parts.push(key.part.dmodp); + } + if (!key.part.dmodq) { + var q = new bigInt(key.part.q.data); + var dmodq = d.mod(q.subtract(1)); + + buf = bigintToMpBuf(dmodq); + key.part.dmodq = {name: 'dmodq', data: buf}; + key.parts.push(key.part.dmodq); + } +} + +function publicFromPrivateECDSA(curveName, priv) { + assert.string(curveName, 'curveName'); + assert.buffer(priv); + if (ec === undefined) + ec = __webpack_require__(139); + if (jsbn === undefined) + jsbn = __webpack_require__(81).BigInteger; + var params = algs.curves[curveName]; + var p = new jsbn(params.p); + var a = new jsbn(params.a); + var b = new jsbn(params.b); + var curve = new ec.ECCurveFp(p, a, b); + var G = curve.decodePointHex(params.G.toString('hex')); + + var d = new jsbn(mpNormalize(priv)); + var pub = G.multiply(d); + pub = Buffer.from(curve.encodePointHex(pub), 'hex'); + + var parts = []; + parts.push({name: 'curve', data: Buffer.from(curveName)}); + parts.push({name: 'Q', data: pub}); + + var key = new Key({type: 'ecdsa', curve: curve, parts: parts}); + return (key); +} + +function opensshCipherInfo(cipher) { + var inf = {}; + switch (cipher) { + case '3des-cbc': + inf.keySize = 24; + inf.blockSize = 8; + inf.opensslName = 'des-ede3-cbc'; + break; + case 'blowfish-cbc': + inf.keySize = 16; + inf.blockSize = 8; + inf.opensslName = 'bf-cbc'; + break; + case 'aes128-cbc': + case 'aes128-ctr': + case 'aes128-gcm@openssh.com': + inf.keySize = 16; + inf.blockSize = 16; + inf.opensslName = 'aes-128-' + cipher.slice(7, 10); + break; + case 'aes192-cbc': + case 'aes192-ctr': + case 'aes192-gcm@openssh.com': + inf.keySize = 24; + inf.blockSize = 16; + inf.opensslName = 'aes-192-' + cipher.slice(7, 10); + break; + case 'aes256-cbc': + case 'aes256-ctr': + case 'aes256-gcm@openssh.com': + inf.keySize = 32; + inf.blockSize = 16; + inf.opensslName = 'aes-256-' + cipher.slice(7, 10); + break; + default: + throw (new Error( + 'Unsupported openssl cipher "' + cipher + '"')); + } + return (inf); +} + + +/***/ }), +/* 27 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = Key; + +var assert = __webpack_require__(16); +var algs = __webpack_require__(32); +var crypto = __webpack_require__(12); +var Fingerprint = __webpack_require__(156); +var Signature = __webpack_require__(74); +var DiffieHellman = __webpack_require__(325).DiffieHellman; +var errs = __webpack_require__(73); +var utils = __webpack_require__(26); +var PrivateKey = __webpack_require__(33); +var edCompat; + +try { + edCompat = __webpack_require__(454); +} catch (e) { + /* Just continue through, and bail out if we try to use it. */ +} + +var InvalidAlgorithmError = errs.InvalidAlgorithmError; +var KeyParseError = errs.KeyParseError; + +var formats = {}; +formats['auto'] = __webpack_require__(455); +formats['pem'] = __webpack_require__(86); +formats['pkcs1'] = __webpack_require__(327); +formats['pkcs8'] = __webpack_require__(157); +formats['rfc4253'] = __webpack_require__(103); +formats['ssh'] = __webpack_require__(456); +formats['ssh-private'] = __webpack_require__(192); +formats['openssh'] = formats['ssh-private']; +formats['dnssec'] = __webpack_require__(326); + +function Key(opts) { + assert.object(opts, 'options'); + assert.arrayOfObject(opts.parts, 'options.parts'); + assert.string(opts.type, 'options.type'); + assert.optionalString(opts.comment, 'options.comment'); + + var algInfo = algs.info[opts.type]; + if (typeof (algInfo) !== 'object') + throw (new InvalidAlgorithmError(opts.type)); + + var partLookup = {}; + for (var i = 0; i < opts.parts.length; ++i) { + var part = opts.parts[i]; + partLookup[part.name] = part; + } + + this.type = opts.type; + this.parts = opts.parts; + this.part = partLookup; + this.comment = undefined; + this.source = opts.source; + + /* for speeding up hashing/fingerprint operations */ + this._rfc4253Cache = opts._rfc4253Cache; + this._hashCache = {}; + + var sz; + this.curve = undefined; + if (this.type === 'ecdsa') { + var curve = this.part.curve.data.toString(); + this.curve = curve; + sz = algs.curves[curve].size; + } else if (this.type === 'ed25519' || this.type === 'curve25519') { + sz = 256; + this.curve = 'curve25519'; + } else { + var szPart = this.part[algInfo.sizePart]; + sz = szPart.data.length; + sz = sz * 8 - utils.countZeros(szPart.data); + } + this.size = sz; +} + +Key.formats = formats; + +Key.prototype.toBuffer = function (format, options) { + if (format === undefined) + format = 'ssh'; + assert.string(format, 'format'); + assert.object(formats[format], 'formats[format]'); + assert.optionalObject(options, 'options'); + + if (format === 'rfc4253') { + if (this._rfc4253Cache === undefined) + this._rfc4253Cache = formats['rfc4253'].write(this); + return (this._rfc4253Cache); + } + + return (formats[format].write(this, options)); +}; + +Key.prototype.toString = function (format, options) { + return (this.toBuffer(format, options).toString()); +}; + +Key.prototype.hash = function (algo) { + assert.string(algo, 'algorithm'); + algo = algo.toLowerCase(); + if (algs.hashAlgs[algo] === undefined) + throw (new InvalidAlgorithmError(algo)); + + if (this._hashCache[algo]) + return (this._hashCache[algo]); + var hash = crypto.createHash(algo). + update(this.toBuffer('rfc4253')).digest(); + this._hashCache[algo] = hash; + return (hash); +}; + +Key.prototype.fingerprint = function (algo) { + if (algo === undefined) + algo = 'sha256'; + assert.string(algo, 'algorithm'); + var opts = { + type: 'key', + hash: this.hash(algo), + algorithm: algo + }; + return (new Fingerprint(opts)); +}; + +Key.prototype.defaultHashAlgorithm = function () { + var hashAlgo = 'sha1'; + if (this.type === 'rsa') + hashAlgo = 'sha256'; + if (this.type === 'dsa' && this.size > 1024) + hashAlgo = 'sha256'; + if (this.type === 'ed25519') + hashAlgo = 'sha512'; + if (this.type === 'ecdsa') { + if (this.size <= 256) + hashAlgo = 'sha256'; + else if (this.size <= 384) + hashAlgo = 'sha384'; + else + hashAlgo = 'sha512'; + } + return (hashAlgo); +}; + +Key.prototype.createVerify = function (hashAlgo) { + if (hashAlgo === undefined) + hashAlgo = this.defaultHashAlgorithm(); + assert.string(hashAlgo, 'hash algorithm'); + + /* ED25519 is not supported by OpenSSL, use a javascript impl. */ + if (this.type === 'ed25519' && edCompat !== undefined) + return (new edCompat.Verifier(this, hashAlgo)); + if (this.type === 'curve25519') + throw (new Error('Curve25519 keys are not suitable for ' + + 'signing or verification')); + + var v, nm, err; + try { + nm = hashAlgo.toUpperCase(); + v = crypto.createVerify(nm); + } catch (e) { + err = e; + } + if (v === undefined || (err instanceof Error && + err.message.match(/Unknown message digest/))) { + nm = 'RSA-'; + nm += hashAlgo.toUpperCase(); + v = crypto.createVerify(nm); + } + assert.ok(v, 'failed to create verifier'); + var oldVerify = v.verify.bind(v); + var key = this.toBuffer('pkcs8'); + var curve = this.curve; + var self = this; + v.verify = function (signature, fmt) { + if (Signature.isSignature(signature, [2, 0])) { + if (signature.type !== self.type) + return (false); + if (signature.hashAlgorithm && + signature.hashAlgorithm !== hashAlgo) + return (false); + if (signature.curve && self.type === 'ecdsa' && + signature.curve !== curve) + return (false); + return (oldVerify(key, signature.toBuffer('asn1'))); + + } else if (typeof (signature) === 'string' || + Buffer.isBuffer(signature)) { + return (oldVerify(key, signature, fmt)); + + /* + * Avoid doing this on valid arguments, walking the prototype + * chain can be quite slow. + */ + } else if (Signature.isSignature(signature, [1, 0])) { + throw (new Error('signature was created by too old ' + + 'a version of sshpk and cannot be verified')); + + } else { + throw (new TypeError('signature must be a string, ' + + 'Buffer, or Signature object')); + } + }; + return (v); +}; + +Key.prototype.createDiffieHellman = function () { + if (this.type === 'rsa') + throw (new Error('RSA keys do not support Diffie-Hellman')); + + return (new DiffieHellman(this)); +}; +Key.prototype.createDH = Key.prototype.createDiffieHellman; + +Key.parse = function (data, format, options) { + if (typeof (data) !== 'string') + assert.buffer(data, 'data'); + if (format === undefined) + format = 'auto'; + assert.string(format, 'format'); + if (typeof (options) === 'string') + options = { filename: options }; + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalString(options.filename, 'options.filename'); + if (options.filename === undefined) + options.filename = '(unnamed)'; + + assert.object(formats[format], 'formats[format]'); + + try { + var k = formats[format].read(data, options); + if (k instanceof PrivateKey) + k = k.toPublic(); + if (!k.comment) + k.comment = options.filename; + return (k); + } catch (e) { + if (e.name === 'KeyEncryptedError') + throw (e); + throw (new KeyParseError(options.filename, format, e)); + } +}; + +Key.isKey = function (obj, ver) { + return (utils.isCompatible(obj, Key, ver)); +}; + +/* + * API versions for Key: + * [1,0] -- initial ver, may take Signature for createVerify or may not + * [1,1] -- added pkcs1, pkcs8 formats + * [1,2] -- added auto, ssh-private, openssh formats + * [1,3] -- added defaultHashAlgorithm + * [1,4] -- added ed support, createDH + * [1,5] -- first explicitly tagged version + * [1,6] -- changed ed25519 part names + */ +Key.prototype._sshpkApiVersion = [1, 6]; + +Key._oldVersionDetect = function (obj) { + assert.func(obj.toBuffer); + assert.func(obj.fingerprint); + if (obj.createDH) + return ([1, 4]); + if (obj.defaultHashAlgorithm) + return ([1, 3]); + if (obj.formats['auto']) + return ([1, 2]); + if (obj.formats['pkcs1']) + return ([1, 1]); + return ([1, 0]); +}; + + +/***/ }), +/* 28 */ +/***/ (function(module, exports) { + +module.exports = require("assert"); + +/***/ }), +/* 29 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = nullify; +function nullify(obj = {}) { + if (Array.isArray(obj)) { + for (var _iterator = obj, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref = _i.value; + } + + const item = _ref; + + nullify(item); + } + } else if (obj !== null && typeof obj === 'object' || typeof obj === 'function') { + Object.setPrototypeOf(obj, null); + + // for..in can only be applied to 'object', not 'function' + if (typeof obj === 'object') { + for (const key in obj) { + nullify(obj[key]); + } + } + } + + return obj; +} + +/***/ }), +/* 30 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const escapeStringRegexp = __webpack_require__(388); +const ansiStyles = __webpack_require__(506); +const stdoutColor = __webpack_require__(598).stdout; + +const template = __webpack_require__(599); + +const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); + +// `supportsColor.level` → `ansiStyles.color[name]` mapping +const levelMapping = ['ansi', 'ansi', 'ansi256', 'ansi16m']; + +// `color-convert` models to exclude from the Chalk API due to conflicts and such +const skipModels = new Set(['gray']); + +const styles = Object.create(null); + +function applyOptions(obj, options) { + options = options || {}; + + // Detect level if not set manually + const scLevel = stdoutColor ? stdoutColor.level : 0; + obj.level = options.level === undefined ? scLevel : options.level; + obj.enabled = 'enabled' in options ? options.enabled : obj.level > 0; +} + +function Chalk(options) { + // We check for this.template here since calling `chalk.constructor()` + // by itself will have a `this` of a previously constructed chalk object + if (!this || !(this instanceof Chalk) || this.template) { + const chalk = {}; + applyOptions(chalk, options); + + chalk.template = function () { + const args = [].slice.call(arguments); + return chalkTag.apply(null, [chalk.template].concat(args)); + }; + + Object.setPrototypeOf(chalk, Chalk.prototype); + Object.setPrototypeOf(chalk.template, chalk); + + chalk.template.constructor = Chalk; + + return chalk.template; + } + + applyOptions(this, options); +} + +// Use bright blue on Windows as the normal blue color is illegible +if (isSimpleWindowsTerm) { + ansiStyles.blue.open = '\u001B[94m'; +} + +for (const key of Object.keys(ansiStyles)) { + ansiStyles[key].closeRe = new RegExp(escapeStringRegexp(ansiStyles[key].close), 'g'); + + styles[key] = { + get() { + const codes = ansiStyles[key]; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, key); + } + }; +} + +styles.visible = { + get() { + return build.call(this, this._styles || [], true, 'visible'); + } +}; + +ansiStyles.color.closeRe = new RegExp(escapeStringRegexp(ansiStyles.color.close), 'g'); +for (const model of Object.keys(ansiStyles.color.ansi)) { + if (skipModels.has(model)) { + continue; + } + + styles[model] = { + get() { + const level = this.level; + return function () { + const open = ansiStyles.color[levelMapping[level]][model].apply(null, arguments); + const codes = { + open, + close: ansiStyles.color.close, + closeRe: ansiStyles.color.closeRe + }; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model); + }; + } + }; +} + +ansiStyles.bgColor.closeRe = new RegExp(escapeStringRegexp(ansiStyles.bgColor.close), 'g'); +for (const model of Object.keys(ansiStyles.bgColor.ansi)) { + if (skipModels.has(model)) { + continue; + } + + const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1); + styles[bgModel] = { + get() { + const level = this.level; + return function () { + const open = ansiStyles.bgColor[levelMapping[level]][model].apply(null, arguments); + const codes = { + open, + close: ansiStyles.bgColor.close, + closeRe: ansiStyles.bgColor.closeRe + }; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model); + }; + } + }; +} + +const proto = Object.defineProperties(() => {}, styles); + +function build(_styles, _empty, key) { + const builder = function () { + return applyStyle.apply(builder, arguments); + }; + + builder._styles = _styles; + builder._empty = _empty; + + const self = this; + + Object.defineProperty(builder, 'level', { + enumerable: true, + get() { + return self.level; + }, + set(level) { + self.level = level; + } + }); + + Object.defineProperty(builder, 'enabled', { + enumerable: true, + get() { + return self.enabled; + }, + set(enabled) { + self.enabled = enabled; + } + }); + + // See below for fix regarding invisible grey/dim combination on Windows + builder.hasGrey = this.hasGrey || key === 'gray' || key === 'grey'; + + // `__proto__` is used because we must return a function, but there is + // no way to create a function with a different prototype + builder.__proto__ = proto; // eslint-disable-line no-proto + + return builder; +} + +function applyStyle() { + // Support varags, but simply cast to string in case there's only one arg + const args = arguments; + const argsLen = args.length; + let str = String(arguments[0]); + + if (argsLen === 0) { + return ''; + } + + if (argsLen > 1) { + // Don't slice `arguments`, it prevents V8 optimizations + for (let a = 1; a < argsLen; a++) { + str += ' ' + args[a]; + } + } + + if (!this.enabled || this.level <= 0 || !str) { + return this._empty ? '' : str; + } + + // Turns out that on Windows dimmed gray text becomes invisible in cmd.exe, + // see https://github.com/chalk/chalk/issues/58 + // If we're on Windows and we're dealing with a gray color, temporarily make 'dim' a noop. + const originalDim = ansiStyles.dim.open; + if (isSimpleWindowsTerm && this.hasGrey) { + ansiStyles.dim.open = ''; + } + + for (const code of this._styles.slice().reverse()) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + str = code.open + str.replace(code.closeRe, code.open) + code.close; + + // Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS + // https://github.com/chalk/chalk/pull/92 + str = str.replace(/\r?\n/g, `${code.close}$&${code.open}`); + } + + // Reset the original `dim` if we changed it to work around the Windows dimmed gray issue + ansiStyles.dim.open = originalDim; + + return str; +} + +function chalkTag(chalk, strings) { + if (!Array.isArray(strings)) { + // If chalk() was called by itself or with a string, + // return the string itself as a string. + return [].slice.call(arguments, 1).join(' '); + } + + const args = [].slice.call(arguments, 2); + const parts = [strings.raw[0]]; + + for (let i = 1; i < strings.length; i++) { + parts.push(String(args[i - 1]).replace(/[{}\\]/g, '\\$&')); + parts.push(String(strings.raw[i])); + } + + return template(chalk, parts.join('')); +} + +Object.defineProperties(Chalk.prototype, styles); + +module.exports = Chalk(); // eslint-disable-line new-cap +module.exports.supportsColor = stdoutColor; +module.exports.default = module.exports; // For TypeScript + + +/***/ }), +/* 31 */ +/***/ (function(module, exports) { + +var core = module.exports = { version: '2.5.7' }; +if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef + + +/***/ }), +/* 32 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +var Buffer = __webpack_require__(15).Buffer; + +var algInfo = { + 'dsa': { + parts: ['p', 'q', 'g', 'y'], + sizePart: 'p' + }, + 'rsa': { + parts: ['e', 'n'], + sizePart: 'n' + }, + 'ecdsa': { + parts: ['curve', 'Q'], + sizePart: 'Q' + }, + 'ed25519': { + parts: ['A'], + sizePart: 'A' + } +}; +algInfo['curve25519'] = algInfo['ed25519']; + +var algPrivInfo = { + 'dsa': { + parts: ['p', 'q', 'g', 'y', 'x'] + }, + 'rsa': { + parts: ['n', 'e', 'd', 'iqmp', 'p', 'q'] + }, + 'ecdsa': { + parts: ['curve', 'Q', 'd'] + }, + 'ed25519': { + parts: ['A', 'k'] + } +}; +algPrivInfo['curve25519'] = algPrivInfo['ed25519']; + +var hashAlgs = { + 'md5': true, + 'sha1': true, + 'sha256': true, + 'sha384': true, + 'sha512': true +}; + +/* + * Taken from + * http://csrc.nist.gov/groups/ST/toolkit/documents/dss/NISTReCur.pdf + */ +var curves = { + 'nistp256': { + size: 256, + pkcs8oid: '1.2.840.10045.3.1.7', + p: Buffer.from(('00' + + 'ffffffff 00000001 00000000 00000000' + + '00000000 ffffffff ffffffff ffffffff'). + replace(/ /g, ''), 'hex'), + a: Buffer.from(('00' + + 'FFFFFFFF 00000001 00000000 00000000' + + '00000000 FFFFFFFF FFFFFFFF FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(( + '5ac635d8 aa3a93e7 b3ebbd55 769886bc' + + '651d06b0 cc53b0f6 3bce3c3e 27d2604b'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'c49d3608 86e70493 6a6678e1 139d26b7' + + '819f7e90'). + replace(/ /g, ''), 'hex'), + n: Buffer.from(('00' + + 'ffffffff 00000000 ffffffff ffffffff' + + 'bce6faad a7179e84 f3b9cac2 fc632551'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + '6b17d1f2 e12c4247 f8bce6e5 63a440f2' + + '77037d81 2deb33a0 f4a13945 d898c296' + + '4fe342e2 fe1a7f9b 8ee7eb4a 7c0f9e16' + + '2bce3357 6b315ece cbb64068 37bf51f5'). + replace(/ /g, ''), 'hex') + }, + 'nistp384': { + size: 384, + pkcs8oid: '1.3.132.0.34', + p: Buffer.from(('00' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff fffffffe' + + 'ffffffff 00000000 00000000 ffffffff'). + replace(/ /g, ''), 'hex'), + a: Buffer.from(('00' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE' + + 'FFFFFFFF 00000000 00000000 FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(( + 'b3312fa7 e23ee7e4 988e056b e3f82d19' + + '181d9c6e fe814112 0314088f 5013875a' + + 'c656398d 8a2ed19d 2a85c8ed d3ec2aef'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'a335926a a319a27a 1d00896a 6773a482' + + '7acdac73'). + replace(/ /g, ''), 'hex'), + n: Buffer.from(('00' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff c7634d81 f4372ddf' + + '581a0db2 48b0a77a ecec196a ccc52973'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + 'aa87ca22 be8b0537 8eb1c71e f320ad74' + + '6e1d3b62 8ba79b98 59f741e0 82542a38' + + '5502f25d bf55296c 3a545e38 72760ab7' + + '3617de4a 96262c6f 5d9e98bf 9292dc29' + + 'f8f41dbd 289a147c e9da3113 b5f0b8c0' + + '0a60b1ce 1d7e819d 7a431d7c 90ea0e5f'). + replace(/ /g, ''), 'hex') + }, + 'nistp521': { + size: 521, + pkcs8oid: '1.3.132.0.35', + p: Buffer.from(( + '01ffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffff').replace(/ /g, ''), 'hex'), + a: Buffer.from(('01FF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(('51' + + '953eb961 8e1c9a1f 929a21a0 b68540ee' + + 'a2da725b 99b315f3 b8b48991 8ef109e1' + + '56193951 ec7e937b 1652c0bd 3bb1bf07' + + '3573df88 3d2c34f1 ef451fd4 6b503f00'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'd09e8800 291cb853 96cc6717 393284aa' + + 'a0da64ba').replace(/ /g, ''), 'hex'), + n: Buffer.from(('01ff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff fffffffa' + + '51868783 bf2f966b 7fcc0148 f709a5d0' + + '3bb5c9b8 899c47ae bb6fb71e 91386409'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + '00c6 858e06b7 0404e9cd 9e3ecb66 2395b442' + + '9c648139 053fb521 f828af60 6b4d3dba' + + 'a14b5e77 efe75928 fe1dc127 a2ffa8de' + + '3348b3c1 856a429b f97e7e31 c2e5bd66' + + '0118 39296a78 9a3bc004 5c8a5fb4 2c7d1bd9' + + '98f54449 579b4468 17afbd17 273e662c' + + '97ee7299 5ef42640 c550b901 3fad0761' + + '353c7086 a272c240 88be9476 9fd16650'). + replace(/ /g, ''), 'hex') + } +}; + +module.exports = { + info: algInfo, + privInfo: algPrivInfo, + hashAlgs: hashAlgs, + curves: curves +}; + + +/***/ }), +/* 33 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = PrivateKey; + +var assert = __webpack_require__(16); +var Buffer = __webpack_require__(15).Buffer; +var algs = __webpack_require__(32); +var crypto = __webpack_require__(12); +var Fingerprint = __webpack_require__(156); +var Signature = __webpack_require__(74); +var errs = __webpack_require__(73); +var util = __webpack_require__(3); +var utils = __webpack_require__(26); +var dhe = __webpack_require__(325); +var generateECDSA = dhe.generateECDSA; +var generateED25519 = dhe.generateED25519; +var edCompat; +var nacl; + +try { + edCompat = __webpack_require__(454); +} catch (e) { + /* Just continue through, and bail out if we try to use it. */ +} + +var Key = __webpack_require__(27); + +var InvalidAlgorithmError = errs.InvalidAlgorithmError; +var KeyParseError = errs.KeyParseError; +var KeyEncryptedError = errs.KeyEncryptedError; + +var formats = {}; +formats['auto'] = __webpack_require__(455); +formats['pem'] = __webpack_require__(86); +formats['pkcs1'] = __webpack_require__(327); +formats['pkcs8'] = __webpack_require__(157); +formats['rfc4253'] = __webpack_require__(103); +formats['ssh-private'] = __webpack_require__(192); +formats['openssh'] = formats['ssh-private']; +formats['ssh'] = formats['ssh-private']; +formats['dnssec'] = __webpack_require__(326); + +function PrivateKey(opts) { + assert.object(opts, 'options'); + Key.call(this, opts); + + this._pubCache = undefined; +} +util.inherits(PrivateKey, Key); + +PrivateKey.formats = formats; + +PrivateKey.prototype.toBuffer = function (format, options) { + if (format === undefined) + format = 'pkcs1'; + assert.string(format, 'format'); + assert.object(formats[format], 'formats[format]'); + assert.optionalObject(options, 'options'); + + return (formats[format].write(this, options)); +}; + +PrivateKey.prototype.hash = function (algo) { + return (this.toPublic().hash(algo)); +}; + +PrivateKey.prototype.toPublic = function () { + if (this._pubCache) + return (this._pubCache); + + var algInfo = algs.info[this.type]; + var pubParts = []; + for (var i = 0; i < algInfo.parts.length; ++i) { + var p = algInfo.parts[i]; + pubParts.push(this.part[p]); + } + + this._pubCache = new Key({ + type: this.type, + source: this, + parts: pubParts + }); + if (this.comment) + this._pubCache.comment = this.comment; + return (this._pubCache); +}; + +PrivateKey.prototype.derive = function (newType) { + assert.string(newType, 'type'); + var priv, pub, pair; + + if (this.type === 'ed25519' && newType === 'curve25519') { + if (nacl === undefined) + nacl = __webpack_require__(75); + + priv = this.part.k.data; + if (priv[0] === 0x00) + priv = priv.slice(1); + + pair = nacl.box.keyPair.fromSecretKey(new Uint8Array(priv)); + pub = Buffer.from(pair.publicKey); + + return (new PrivateKey({ + type: 'curve25519', + parts: [ + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } + ] + })); + } else if (this.type === 'curve25519' && newType === 'ed25519') { + if (nacl === undefined) + nacl = __webpack_require__(75); + + priv = this.part.k.data; + if (priv[0] === 0x00) + priv = priv.slice(1); + + pair = nacl.sign.keyPair.fromSeed(new Uint8Array(priv)); + pub = Buffer.from(pair.publicKey); + + return (new PrivateKey({ + type: 'ed25519', + parts: [ + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } + ] + })); + } + throw (new Error('Key derivation not supported from ' + this.type + + ' to ' + newType)); +}; + +PrivateKey.prototype.createVerify = function (hashAlgo) { + return (this.toPublic().createVerify(hashAlgo)); +}; + +PrivateKey.prototype.createSign = function (hashAlgo) { + if (hashAlgo === undefined) + hashAlgo = this.defaultHashAlgorithm(); + assert.string(hashAlgo, 'hash algorithm'); + + /* ED25519 is not supported by OpenSSL, use a javascript impl. */ + if (this.type === 'ed25519' && edCompat !== undefined) + return (new edCompat.Signer(this, hashAlgo)); + if (this.type === 'curve25519') + throw (new Error('Curve25519 keys are not suitable for ' + + 'signing or verification')); + + var v, nm, err; + try { + nm = hashAlgo.toUpperCase(); + v = crypto.createSign(nm); + } catch (e) { + err = e; + } + if (v === undefined || (err instanceof Error && + err.message.match(/Unknown message digest/))) { + nm = 'RSA-'; + nm += hashAlgo.toUpperCase(); + v = crypto.createSign(nm); + } + assert.ok(v, 'failed to create verifier'); + var oldSign = v.sign.bind(v); + var key = this.toBuffer('pkcs1'); + var type = this.type; + var curve = this.curve; + v.sign = function () { + var sig = oldSign(key); + if (typeof (sig) === 'string') + sig = Buffer.from(sig, 'binary'); + sig = Signature.parse(sig, type, 'asn1'); + sig.hashAlgorithm = hashAlgo; + sig.curve = curve; + return (sig); + }; + return (v); +}; + +PrivateKey.parse = function (data, format, options) { + if (typeof (data) !== 'string') + assert.buffer(data, 'data'); + if (format === undefined) + format = 'auto'; + assert.string(format, 'format'); + if (typeof (options) === 'string') + options = { filename: options }; + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalString(options.filename, 'options.filename'); + if (options.filename === undefined) + options.filename = '(unnamed)'; + + assert.object(formats[format], 'formats[format]'); + + try { + var k = formats[format].read(data, options); + assert.ok(k instanceof PrivateKey, 'key is not a private key'); + if (!k.comment) + k.comment = options.filename; + return (k); + } catch (e) { + if (e.name === 'KeyEncryptedError') + throw (e); + throw (new KeyParseError(options.filename, format, e)); + } +}; + +PrivateKey.isPrivateKey = function (obj, ver) { + return (utils.isCompatible(obj, PrivateKey, ver)); +}; + +PrivateKey.generate = function (type, options) { + if (options === undefined) + options = {}; + assert.object(options, 'options'); + + switch (type) { + case 'ecdsa': + if (options.curve === undefined) + options.curve = 'nistp256'; + assert.string(options.curve, 'options.curve'); + return (generateECDSA(options.curve)); + case 'ed25519': + return (generateED25519()); + default: + throw (new Error('Key generation not supported with key ' + + 'type "' + type + '"')); + } +}; + +/* + * API versions for PrivateKey: + * [1,0] -- initial ver + * [1,1] -- added auto, pkcs[18], openssh/ssh-private formats + * [1,2] -- added defaultHashAlgorithm + * [1,3] -- added derive, ed, createDH + * [1,4] -- first tagged version + * [1,5] -- changed ed25519 part names and format + */ +PrivateKey.prototype._sshpkApiVersion = [1, 5]; + +PrivateKey._oldVersionDetect = function (obj) { + assert.func(obj.toPublic); + assert.func(obj.createSign); + if (obj.derive) + return ([1, 3]); + if (obj.defaultHashAlgorithm) + return ([1, 2]); + if (obj.formats['auto']) + return ([1, 1]); + return ([1, 0]); +}; + + +/***/ }), +/* 34 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.wrapLifecycle = exports.run = exports.install = exports.Install = undefined; + +var _extends2; + +function _load_extends() { + return _extends2 = _interopRequireDefault(__webpack_require__(22)); +} + +var _asyncToGenerator2; + +function _load_asyncToGenerator() { + return _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(2)); +} + +let install = exports.install = (() => { + var _ref29 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (config, reporter, flags, lockfile) { + yield wrapLifecycle(config, flags, (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const install = new Install(flags, config, reporter, lockfile); + yield install.init(); + })); + }); + + return function install(_x7, _x8, _x9, _x10) { + return _ref29.apply(this, arguments); + }; +})(); + +let run = exports.run = (() => { + var _ref31 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (config, reporter, flags, args) { + let lockfile; + let error = 'installCommandRenamed'; + if (flags.lockfile === false) { + lockfile = new (_lockfile || _load_lockfile()).default(); + } else { + lockfile = yield (_lockfile || _load_lockfile()).default.fromDirectory(config.lockfileFolder, reporter); + } + + if (args.length) { + const exampleArgs = args.slice(); + + if (flags.saveDev) { + exampleArgs.push('--dev'); + } + if (flags.savePeer) { + exampleArgs.push('--peer'); + } + if (flags.saveOptional) { + exampleArgs.push('--optional'); + } + if (flags.saveExact) { + exampleArgs.push('--exact'); + } + if (flags.saveTilde) { + exampleArgs.push('--tilde'); + } + let command = 'add'; + if (flags.global) { + error = 'globalFlagRemoved'; + command = 'global add'; + } + throw new (_errors || _load_errors()).MessageError(reporter.lang(error, `yarn ${command} ${exampleArgs.join(' ')}`)); + } + + yield install(config, reporter, flags, lockfile); + }); + + return function run(_x11, _x12, _x13, _x14) { + return _ref31.apply(this, arguments); + }; +})(); + +let wrapLifecycle = exports.wrapLifecycle = (() => { + var _ref32 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (config, flags, factory) { + yield config.executeLifecycleScript('preinstall'); + + yield factory(); + + // npm behaviour, seems kinda funky but yay compatibility + yield config.executeLifecycleScript('install'); + yield config.executeLifecycleScript('postinstall'); + + if (!config.production) { + if (!config.disablePrepublish) { + yield config.executeLifecycleScript('prepublish'); + } + yield config.executeLifecycleScript('prepare'); + } + }); + + return function wrapLifecycle(_x15, _x16, _x17) { + return _ref32.apply(this, arguments); + }; +})(); + +exports.hasWrapper = hasWrapper; +exports.setFlags = setFlags; + +var _objectPath; + +function _load_objectPath() { + return _objectPath = _interopRequireDefault(__webpack_require__(304)); +} + +var _hooks; + +function _load_hooks() { + return _hooks = __webpack_require__(374); +} + +var _index; + +function _load_index() { + return _index = _interopRequireDefault(__webpack_require__(220)); +} + +var _errors; + +function _load_errors() { + return _errors = __webpack_require__(6); +} + +var _integrityChecker; + +function _load_integrityChecker() { + return _integrityChecker = _interopRequireDefault(__webpack_require__(208)); +} + +var _lockfile; + +function _load_lockfile() { + return _lockfile = _interopRequireDefault(__webpack_require__(19)); +} + +var _lockfile2; + +function _load_lockfile2() { + return _lockfile2 = __webpack_require__(19); +} + +var _packageFetcher; + +function _load_packageFetcher() { + return _packageFetcher = _interopRequireWildcard(__webpack_require__(210)); +} + +var _packageInstallScripts; + +function _load_packageInstallScripts() { + return _packageInstallScripts = _interopRequireDefault(__webpack_require__(557)); +} + +var _packageCompatibility; + +function _load_packageCompatibility() { + return _packageCompatibility = _interopRequireWildcard(__webpack_require__(209)); +} + +var _packageResolver; + +function _load_packageResolver() { + return _packageResolver = _interopRequireDefault(__webpack_require__(366)); +} + +var _packageLinker; + +function _load_packageLinker() { + return _packageLinker = _interopRequireDefault(__webpack_require__(211)); +} + +var _index2; + +function _load_index2() { + return _index2 = __webpack_require__(57); +} + +var _index3; + +function _load_index3() { + return _index3 = __webpack_require__(78); +} + +var _autoclean; + +function _load_autoclean() { + return _autoclean = __webpack_require__(354); +} + +var _constants; + +function _load_constants() { + return _constants = _interopRequireWildcard(__webpack_require__(8)); +} + +var _normalizePattern; + +function _load_normalizePattern() { + return _normalizePattern = __webpack_require__(37); +} + +var _fs; + +function _load_fs() { + return _fs = _interopRequireWildcard(__webpack_require__(4)); +} + +var _map; + +function _load_map() { + return _map = _interopRequireDefault(__webpack_require__(29)); +} + +var _yarnVersion; + +function _load_yarnVersion() { + return _yarnVersion = __webpack_require__(120); +} + +var _generatePnpMap; + +function _load_generatePnpMap() { + return _generatePnpMap = __webpack_require__(579); +} + +var _workspaceLayout; + +function _load_workspaceLayout() { + return _workspaceLayout = _interopRequireDefault(__webpack_require__(90)); +} + +var _resolutionMap; + +function _load_resolutionMap() { + return _resolutionMap = _interopRequireDefault(__webpack_require__(214)); +} + +var _guessName; + +function _load_guessName() { + return _guessName = _interopRequireDefault(__webpack_require__(169)); +} + +var _audit; + +function _load_audit() { + return _audit = _interopRequireDefault(__webpack_require__(353)); +} + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const deepEqual = __webpack_require__(631); + +const emoji = __webpack_require__(302); +const invariant = __webpack_require__(9); +const path = __webpack_require__(0); +const semver = __webpack_require__(21); +const uuid = __webpack_require__(119); +const ssri = __webpack_require__(77); + +const ONE_DAY = 1000 * 60 * 60 * 24; + +/** + * Try and detect the installation method for Yarn and provide a command to update it with. + */ + +function getUpdateCommand(installationMethod) { + if (installationMethod === 'tar') { + return `curl --compressed -o- -L ${(_constants || _load_constants()).YARN_INSTALLER_SH} | bash`; + } + + if (installationMethod === 'homebrew') { + return 'brew upgrade yarn'; + } + + if (installationMethod === 'deb') { + return 'sudo apt-get update && sudo apt-get install yarn'; + } + + if (installationMethod === 'rpm') { + return 'sudo yum install yarn'; + } + + if (installationMethod === 'npm') { + return 'npm install --global yarn'; + } + + if (installationMethod === 'chocolatey') { + return 'choco upgrade yarn'; + } + + if (installationMethod === 'apk') { + return 'apk update && apk add -u yarn'; + } + + if (installationMethod === 'portage') { + return 'sudo emerge --sync && sudo emerge -au sys-apps/yarn'; + } + + return null; +} + +function getUpdateInstaller(installationMethod) { + // Windows + if (installationMethod === 'msi') { + return (_constants || _load_constants()).YARN_INSTALLER_MSI; + } + + return null; +} + +function normalizeFlags(config, rawFlags) { + const flags = { + // install + har: !!rawFlags.har, + ignorePlatform: !!rawFlags.ignorePlatform, + ignoreEngines: !!rawFlags.ignoreEngines, + ignoreScripts: !!rawFlags.ignoreScripts, + ignoreOptional: !!rawFlags.ignoreOptional, + force: !!rawFlags.force, + flat: !!rawFlags.flat, + lockfile: rawFlags.lockfile !== false, + pureLockfile: !!rawFlags.pureLockfile, + updateChecksums: !!rawFlags.updateChecksums, + skipIntegrityCheck: !!rawFlags.skipIntegrityCheck, + frozenLockfile: !!rawFlags.frozenLockfile, + linkDuplicates: !!rawFlags.linkDuplicates, + checkFiles: !!rawFlags.checkFiles, + audit: !!rawFlags.audit, + + // add + peer: !!rawFlags.peer, + dev: !!rawFlags.dev, + optional: !!rawFlags.optional, + exact: !!rawFlags.exact, + tilde: !!rawFlags.tilde, + ignoreWorkspaceRootCheck: !!rawFlags.ignoreWorkspaceRootCheck, + + // outdated, update-interactive + includeWorkspaceDeps: !!rawFlags.includeWorkspaceDeps, + + // add, remove, update + workspaceRootIsCwd: rawFlags.workspaceRootIsCwd !== false + }; + + if (config.getOption('ignore-scripts')) { + flags.ignoreScripts = true; + } + + if (config.getOption('ignore-platform')) { + flags.ignorePlatform = true; + } + + if (config.getOption('ignore-engines')) { + flags.ignoreEngines = true; + } + + if (config.getOption('ignore-optional')) { + flags.ignoreOptional = true; + } + + if (config.getOption('force')) { + flags.force = true; + } + + return flags; +} + +class Install { + constructor(flags, config, reporter, lockfile) { + this.rootManifestRegistries = []; + this.rootPatternsToOrigin = (0, (_map || _load_map()).default)(); + this.lockfile = lockfile; + this.reporter = reporter; + this.config = config; + this.flags = normalizeFlags(config, flags); + this.resolutions = (0, (_map || _load_map()).default)(); // Legacy resolutions field used for flat install mode + this.resolutionMap = new (_resolutionMap || _load_resolutionMap()).default(config); // Selective resolutions for nested dependencies + this.resolver = new (_packageResolver || _load_packageResolver()).default(config, lockfile, this.resolutionMap); + this.integrityChecker = new (_integrityChecker || _load_integrityChecker()).default(config); + this.linker = new (_packageLinker || _load_packageLinker()).default(config, this.resolver); + this.scripts = new (_packageInstallScripts || _load_packageInstallScripts()).default(config, this.resolver, this.flags.force); + } + + /** + * Create a list of dependency requests from the current directories manifests. + */ + + fetchRequestFromCwd(excludePatterns = [], ignoreUnusedPatterns = false) { + var _this = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const patterns = []; + const deps = []; + let resolutionDeps = []; + const manifest = {}; + + const ignorePatterns = []; + const usedPatterns = []; + let workspaceLayout; + + // some commands should always run in the context of the entire workspace + const cwd = _this.flags.includeWorkspaceDeps || _this.flags.workspaceRootIsCwd ? _this.config.lockfileFolder : _this.config.cwd; + + // non-workspaces are always root, otherwise check for workspace root + const cwdIsRoot = !_this.config.workspaceRootFolder || _this.config.lockfileFolder === cwd; + + // exclude package names that are in install args + const excludeNames = []; + for (var _iterator = excludePatterns, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref = _i.value; + } + + const pattern = _ref; + + if ((0, (_index3 || _load_index3()).getExoticResolver)(pattern)) { + excludeNames.push((0, (_guessName || _load_guessName()).default)(pattern)); + } else { + // extract the name + const parts = (0, (_normalizePattern || _load_normalizePattern()).normalizePattern)(pattern); + excludeNames.push(parts.name); + } + } + + const stripExcluded = function stripExcluded(manifest) { + for (var _iterator2 = excludeNames, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { + var _ref2; + + if (_isArray2) { + if (_i2 >= _iterator2.length) break; + _ref2 = _iterator2[_i2++]; + } else { + _i2 = _iterator2.next(); + if (_i2.done) break; + _ref2 = _i2.value; + } + + const exclude = _ref2; + + if (manifest.dependencies && manifest.dependencies[exclude]) { + delete manifest.dependencies[exclude]; + } + if (manifest.devDependencies && manifest.devDependencies[exclude]) { + delete manifest.devDependencies[exclude]; + } + if (manifest.optionalDependencies && manifest.optionalDependencies[exclude]) { + delete manifest.optionalDependencies[exclude]; + } + } + }; + + for (var _iterator3 = Object.keys((_index2 || _load_index2()).registries), _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { + var _ref3; + + if (_isArray3) { + if (_i3 >= _iterator3.length) break; + _ref3 = _iterator3[_i3++]; + } else { + _i3 = _iterator3.next(); + if (_i3.done) break; + _ref3 = _i3.value; + } + + const registry = _ref3; + + const filename = (_index2 || _load_index2()).registries[registry].filename; + + const loc = path.join(cwd, filename); + if (!(yield (_fs || _load_fs()).exists(loc))) { + continue; + } + + _this.rootManifestRegistries.push(registry); + + const projectManifestJson = yield _this.config.readJson(loc); + yield (0, (_index || _load_index()).default)(projectManifestJson, cwd, _this.config, cwdIsRoot); + + Object.assign(_this.resolutions, projectManifestJson.resolutions); + Object.assign(manifest, projectManifestJson); + + _this.resolutionMap.init(_this.resolutions); + for (var _iterator4 = Object.keys(_this.resolutionMap.resolutionsByPackage), _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { + var _ref4; + + if (_isArray4) { + if (_i4 >= _iterator4.length) break; + _ref4 = _iterator4[_i4++]; + } else { + _i4 = _iterator4.next(); + if (_i4.done) break; + _ref4 = _i4.value; + } + + const packageName = _ref4; + + const optional = (_objectPath || _load_objectPath()).default.has(manifest.optionalDependencies, packageName) && _this.flags.ignoreOptional; + for (var _iterator8 = _this.resolutionMap.resolutionsByPackage[packageName], _isArray8 = Array.isArray(_iterator8), _i8 = 0, _iterator8 = _isArray8 ? _iterator8 : _iterator8[Symbol.iterator]();;) { + var _ref9; + + if (_isArray8) { + if (_i8 >= _iterator8.length) break; + _ref9 = _iterator8[_i8++]; + } else { + _i8 = _iterator8.next(); + if (_i8.done) break; + _ref9 = _i8.value; + } + + const _ref8 = _ref9; + const pattern = _ref8.pattern; + + resolutionDeps = [...resolutionDeps, { registry, pattern, optional, hint: 'resolution' }]; + } + } + + const pushDeps = function pushDeps(depType, manifest, { hint, optional }, isUsed) { + if (ignoreUnusedPatterns && !isUsed) { + return; + } + // We only take unused dependencies into consideration to get deterministic hoisting. + // Since flat mode doesn't care about hoisting and everything is top level and specified then we can safely + // leave these out. + if (_this.flags.flat && !isUsed) { + return; + } + const depMap = manifest[depType]; + for (const name in depMap) { + if (excludeNames.indexOf(name) >= 0) { + continue; + } + + let pattern = name; + if (!_this.lockfile.getLocked(pattern)) { + // when we use --save we save the dependency to the lockfile with just the name rather than the + // version combo + pattern += '@' + depMap[name]; + } + + // normalization made sure packages are mentioned only once + if (isUsed) { + usedPatterns.push(pattern); + } else { + ignorePatterns.push(pattern); + } + + _this.rootPatternsToOrigin[pattern] = depType; + patterns.push(pattern); + deps.push({ pattern, registry, hint, optional, workspaceName: manifest.name, workspaceLoc: manifest._loc }); + } + }; + + if (cwdIsRoot) { + pushDeps('dependencies', projectManifestJson, { hint: null, optional: false }, true); + pushDeps('devDependencies', projectManifestJson, { hint: 'dev', optional: false }, !_this.config.production); + pushDeps('optionalDependencies', projectManifestJson, { hint: 'optional', optional: true }, true); + } + + if (_this.config.workspaceRootFolder) { + const workspaceLoc = cwdIsRoot ? loc : path.join(_this.config.lockfileFolder, filename); + const workspacesRoot = path.dirname(workspaceLoc); + + let workspaceManifestJson = projectManifestJson; + if (!cwdIsRoot) { + // the manifest we read before was a child workspace, so get the root + workspaceManifestJson = yield _this.config.readJson(workspaceLoc); + yield (0, (_index || _load_index()).default)(workspaceManifestJson, workspacesRoot, _this.config, true); + } + + const workspaces = yield _this.config.resolveWorkspaces(workspacesRoot, workspaceManifestJson); + workspaceLayout = new (_workspaceLayout || _load_workspaceLayout()).default(workspaces, _this.config); + + // add virtual manifest that depends on all workspaces, this way package hoisters and resolvers will work fine + const workspaceDependencies = (0, (_extends2 || _load_extends()).default)({}, workspaceManifestJson.dependencies); + for (var _iterator5 = Object.keys(workspaces), _isArray5 = Array.isArray(_iterator5), _i5 = 0, _iterator5 = _isArray5 ? _iterator5 : _iterator5[Symbol.iterator]();;) { + var _ref5; + + if (_isArray5) { + if (_i5 >= _iterator5.length) break; + _ref5 = _iterator5[_i5++]; + } else { + _i5 = _iterator5.next(); + if (_i5.done) break; + _ref5 = _i5.value; + } + + const workspaceName = _ref5; + + const workspaceManifest = workspaces[workspaceName].manifest; + workspaceDependencies[workspaceName] = workspaceManifest.version; + + // include dependencies from all workspaces + if (_this.flags.includeWorkspaceDeps) { + pushDeps('dependencies', workspaceManifest, { hint: null, optional: false }, true); + pushDeps('devDependencies', workspaceManifest, { hint: 'dev', optional: false }, !_this.config.production); + pushDeps('optionalDependencies', workspaceManifest, { hint: 'optional', optional: true }, true); + } + } + const virtualDependencyManifest = { + _uid: '', + name: `workspace-aggregator-${uuid.v4()}`, + version: '1.0.0', + _registry: 'npm', + _loc: workspacesRoot, + dependencies: workspaceDependencies, + devDependencies: (0, (_extends2 || _load_extends()).default)({}, workspaceManifestJson.devDependencies), + optionalDependencies: (0, (_extends2 || _load_extends()).default)({}, workspaceManifestJson.optionalDependencies), + private: workspaceManifestJson.private, + workspaces: workspaceManifestJson.workspaces + }; + workspaceLayout.virtualManifestName = virtualDependencyManifest.name; + const virtualDep = {}; + virtualDep[virtualDependencyManifest.name] = virtualDependencyManifest.version; + workspaces[virtualDependencyManifest.name] = { loc: workspacesRoot, manifest: virtualDependencyManifest }; + + // ensure dependencies that should be excluded are stripped from the correct manifest + stripExcluded(cwdIsRoot ? virtualDependencyManifest : workspaces[projectManifestJson.name].manifest); + + pushDeps('workspaces', { workspaces: virtualDep }, { hint: 'workspaces', optional: false }, true); + + const implicitWorkspaceDependencies = (0, (_extends2 || _load_extends()).default)({}, workspaceDependencies); + + for (var _iterator6 = (_constants || _load_constants()).OWNED_DEPENDENCY_TYPES, _isArray6 = Array.isArray(_iterator6), _i6 = 0, _iterator6 = _isArray6 ? _iterator6 : _iterator6[Symbol.iterator]();;) { + var _ref6; + + if (_isArray6) { + if (_i6 >= _iterator6.length) break; + _ref6 = _iterator6[_i6++]; + } else { + _i6 = _iterator6.next(); + if (_i6.done) break; + _ref6 = _i6.value; + } + + const type = _ref6; + + for (var _iterator7 = Object.keys(projectManifestJson[type] || {}), _isArray7 = Array.isArray(_iterator7), _i7 = 0, _iterator7 = _isArray7 ? _iterator7 : _iterator7[Symbol.iterator]();;) { + var _ref7; + + if (_isArray7) { + if (_i7 >= _iterator7.length) break; + _ref7 = _iterator7[_i7++]; + } else { + _i7 = _iterator7.next(); + if (_i7.done) break; + _ref7 = _i7.value; + } + + const dependencyName = _ref7; + + delete implicitWorkspaceDependencies[dependencyName]; + } + } + + pushDeps('dependencies', { dependencies: implicitWorkspaceDependencies }, { hint: 'workspaces', optional: false }, true); + } + + break; + } + + // inherit root flat flag + if (manifest.flat) { + _this.flags.flat = true; + } + + return { + requests: [...resolutionDeps, ...deps], + patterns, + manifest, + usedPatterns, + ignorePatterns, + workspaceLayout + }; + })(); + } + + /** + * TODO description + */ + + prepareRequests(requests) { + return requests; + } + + preparePatterns(patterns) { + return patterns; + } + preparePatternsForLinking(patterns, cwdManifest, cwdIsRoot) { + return patterns; + } + + prepareManifests() { + var _this2 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const manifests = yield _this2.config.getRootManifests(); + return manifests; + })(); + } + + bailout(patterns, workspaceLayout) { + var _this3 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // We don't want to skip the audit - it could yield important errors + if (_this3.flags.audit) { + return false; + } + // PNP is so fast that the integrity check isn't pertinent + if (_this3.config.plugnplayEnabled) { + return false; + } + if (_this3.flags.skipIntegrityCheck || _this3.flags.force) { + return false; + } + const lockfileCache = _this3.lockfile.cache; + if (!lockfileCache) { + return false; + } + const lockfileClean = _this3.lockfile.parseResultType === 'success'; + const match = yield _this3.integrityChecker.check(patterns, lockfileCache, _this3.flags, workspaceLayout); + if (_this3.flags.frozenLockfile && (!lockfileClean || match.missingPatterns.length > 0)) { + throw new (_errors || _load_errors()).MessageError(_this3.reporter.lang('frozenLockfileError')); + } + + const haveLockfile = yield (_fs || _load_fs()).exists(path.join(_this3.config.lockfileFolder, (_constants || _load_constants()).LOCKFILE_FILENAME)); + + const lockfileIntegrityPresent = !_this3.lockfile.hasEntriesExistWithoutIntegrity(); + const integrityBailout = lockfileIntegrityPresent || !_this3.config.autoAddIntegrity; + + if (match.integrityMatches && haveLockfile && lockfileClean && integrityBailout) { + _this3.reporter.success(_this3.reporter.lang('upToDate')); + return true; + } + + if (match.integrityFileMissing && haveLockfile) { + // Integrity file missing, force script installations + _this3.scripts.setForce(true); + return false; + } + + if (match.hardRefreshRequired) { + // e.g. node version doesn't match, force script installations + _this3.scripts.setForce(true); + return false; + } + + if (!patterns.length && !match.integrityFileMissing) { + _this3.reporter.success(_this3.reporter.lang('nothingToInstall')); + yield _this3.createEmptyManifestFolders(); + yield _this3.saveLockfileAndIntegrity(patterns, workspaceLayout); + return true; + } + + return false; + })(); + } + + /** + * Produce empty folders for all used root manifests. + */ + + createEmptyManifestFolders() { + var _this4 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + if (_this4.config.modulesFolder) { + // already created + return; + } + + for (var _iterator9 = _this4.rootManifestRegistries, _isArray9 = Array.isArray(_iterator9), _i9 = 0, _iterator9 = _isArray9 ? _iterator9 : _iterator9[Symbol.iterator]();;) { + var _ref10; + + if (_isArray9) { + if (_i9 >= _iterator9.length) break; + _ref10 = _iterator9[_i9++]; + } else { + _i9 = _iterator9.next(); + if (_i9.done) break; + _ref10 = _i9.value; + } + + const registryName = _ref10; + const folder = _this4.config.registries[registryName].folder; + + yield (_fs || _load_fs()).mkdirp(path.join(_this4.config.lockfileFolder, folder)); + } + })(); + } + + /** + * TODO description + */ + + markIgnored(patterns) { + for (var _iterator10 = patterns, _isArray10 = Array.isArray(_iterator10), _i10 = 0, _iterator10 = _isArray10 ? _iterator10 : _iterator10[Symbol.iterator]();;) { + var _ref11; + + if (_isArray10) { + if (_i10 >= _iterator10.length) break; + _ref11 = _iterator10[_i10++]; + } else { + _i10 = _iterator10.next(); + if (_i10.done) break; + _ref11 = _i10.value; + } + + const pattern = _ref11; + + const manifest = this.resolver.getStrictResolvedPattern(pattern); + const ref = manifest._reference; + invariant(ref, 'expected package reference'); + + // just mark the package as ignored. if the package is used by a required package, the hoister + // will take care of that. + ref.ignore = true; + } + } + + /** + * helper method that gets only recent manifests + * used by global.ls command + */ + getFlattenedDeps() { + var _this5 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + var _ref12 = yield _this5.fetchRequestFromCwd(); + + const depRequests = _ref12.requests, + rawPatterns = _ref12.patterns; + + + yield _this5.resolver.init(depRequests, {}); + + const manifests = yield (_packageFetcher || _load_packageFetcher()).fetch(_this5.resolver.getManifests(), _this5.config); + _this5.resolver.updateManifests(manifests); + + return _this5.flatten(rawPatterns); + })(); + } + + /** + * TODO description + */ + + init() { + var _this6 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.checkUpdate(); + + // warn if we have a shrinkwrap + if (yield (_fs || _load_fs()).exists(path.join(_this6.config.lockfileFolder, (_constants || _load_constants()).NPM_SHRINKWRAP_FILENAME))) { + _this6.reporter.warn(_this6.reporter.lang('shrinkwrapWarning')); + } + + // warn if we have an npm lockfile + if (yield (_fs || _load_fs()).exists(path.join(_this6.config.lockfileFolder, (_constants || _load_constants()).NPM_LOCK_FILENAME))) { + _this6.reporter.warn(_this6.reporter.lang('npmLockfileWarning')); + } + + if (_this6.config.plugnplayEnabled) { + _this6.reporter.info(_this6.reporter.lang('plugnplaySuggestV2L1')); + _this6.reporter.info(_this6.reporter.lang('plugnplaySuggestV2L2')); + } + + let flattenedTopLevelPatterns = []; + const steps = []; + + var _ref13 = yield _this6.fetchRequestFromCwd(); + + const depRequests = _ref13.requests, + rawPatterns = _ref13.patterns, + ignorePatterns = _ref13.ignorePatterns, + workspaceLayout = _ref13.workspaceLayout, + manifest = _ref13.manifest; + + let topLevelPatterns = []; + + const artifacts = yield _this6.integrityChecker.getArtifacts(); + if (artifacts) { + _this6.linker.setArtifacts(artifacts); + _this6.scripts.setArtifacts(artifacts); + } + + if ((_packageCompatibility || _load_packageCompatibility()).shouldCheck(manifest, _this6.flags)) { + steps.push((() => { + var _ref14 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (curr, total) { + _this6.reporter.step(curr, total, _this6.reporter.lang('checkingManifest'), emoji.get('mag')); + yield _this6.checkCompatibility(); + }); + + return function (_x, _x2) { + return _ref14.apply(this, arguments); + }; + })()); + } + + const audit = new (_audit || _load_audit()).default(_this6.config, _this6.reporter, { groups: (_constants || _load_constants()).OWNED_DEPENDENCY_TYPES }); + let auditFoundProblems = false; + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('resolveStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.reporter.step(curr, total, _this6.reporter.lang('resolvingPackages'), emoji.get('mag')); + yield _this6.resolver.init(_this6.prepareRequests(depRequests), { + isFlat: _this6.flags.flat, + isFrozen: _this6.flags.frozenLockfile, + workspaceLayout + }); + topLevelPatterns = _this6.preparePatterns(rawPatterns); + flattenedTopLevelPatterns = yield _this6.flatten(topLevelPatterns); + return { bailout: !_this6.flags.audit && (yield _this6.bailout(topLevelPatterns, workspaceLayout)) }; + })); + }); + + if (_this6.flags.audit) { + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('auditStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.reporter.step(curr, total, _this6.reporter.lang('auditRunning'), emoji.get('mag')); + if (_this6.flags.offline) { + _this6.reporter.warn(_this6.reporter.lang('auditOffline')); + return { bailout: false }; + } + const preparedManifests = yield _this6.prepareManifests(); + // $FlowFixMe - Flow considers `m` in the map operation to be "mixed", so does not recognize `m.object` + const mergedManifest = Object.assign({}, ...Object.values(preparedManifests).map(function (m) { + return m.object; + })); + const auditVulnerabilityCounts = yield audit.performAudit(mergedManifest, _this6.lockfile, _this6.resolver, _this6.linker, topLevelPatterns); + auditFoundProblems = auditVulnerabilityCounts.info || auditVulnerabilityCounts.low || auditVulnerabilityCounts.moderate || auditVulnerabilityCounts.high || auditVulnerabilityCounts.critical; + return { bailout: yield _this6.bailout(topLevelPatterns, workspaceLayout) }; + })); + }); + } + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('fetchStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.markIgnored(ignorePatterns); + _this6.reporter.step(curr, total, _this6.reporter.lang('fetchingPackages'), emoji.get('truck')); + const manifests = yield (_packageFetcher || _load_packageFetcher()).fetch(_this6.resolver.getManifests(), _this6.config); + _this6.resolver.updateManifests(manifests); + yield (_packageCompatibility || _load_packageCompatibility()).check(_this6.resolver.getManifests(), _this6.config, _this6.flags.ignoreEngines); + })); + }); + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('linkStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // remove integrity hash to make this operation atomic + yield _this6.integrityChecker.removeIntegrityFile(); + _this6.reporter.step(curr, total, _this6.reporter.lang('linkingDependencies'), emoji.get('link')); + flattenedTopLevelPatterns = _this6.preparePatternsForLinking(flattenedTopLevelPatterns, manifest, _this6.config.lockfileFolder === _this6.config.cwd); + yield _this6.linker.init(flattenedTopLevelPatterns, workspaceLayout, { + linkDuplicates: _this6.flags.linkDuplicates, + ignoreOptional: _this6.flags.ignoreOptional + }); + })); + }); + + if (_this6.config.plugnplayEnabled) { + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('pnpStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const pnpPath = `${_this6.config.lockfileFolder}/${(_constants || _load_constants()).PNP_FILENAME}`; + + const code = yield (0, (_generatePnpMap || _load_generatePnpMap()).generatePnpMap)(_this6.config, flattenedTopLevelPatterns, { + resolver: _this6.resolver, + reporter: _this6.reporter, + targetPath: pnpPath, + workspaceLayout + }); + + try { + const file = yield (_fs || _load_fs()).readFile(pnpPath); + if (file === code) { + return; + } + } catch (error) {} + + yield (_fs || _load_fs()).writeFile(pnpPath, code); + yield (_fs || _load_fs()).chmod(pnpPath, 0o755); + })); + }); + } + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('buildStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.reporter.step(curr, total, _this6.flags.force ? _this6.reporter.lang('rebuildingPackages') : _this6.reporter.lang('buildingFreshPackages'), emoji.get('hammer')); + + if (_this6.config.ignoreScripts) { + _this6.reporter.warn(_this6.reporter.lang('ignoredScripts')); + } else { + yield _this6.scripts.init(flattenedTopLevelPatterns); + } + })); + }); + + if (_this6.flags.har) { + steps.push((() => { + var _ref21 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (curr, total) { + const formattedDate = new Date().toISOString().replace(/:/g, '-'); + const filename = `yarn-install_${formattedDate}.har`; + _this6.reporter.step(curr, total, _this6.reporter.lang('savingHar', filename), emoji.get('black_circle_for_record')); + yield _this6.config.requestManager.saveHar(filename); + }); + + return function (_x3, _x4) { + return _ref21.apply(this, arguments); + }; + })()); + } + + if (yield _this6.shouldClean()) { + steps.push((() => { + var _ref22 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (curr, total) { + _this6.reporter.step(curr, total, _this6.reporter.lang('cleaningModules'), emoji.get('recycle')); + yield (0, (_autoclean || _load_autoclean()).clean)(_this6.config, _this6.reporter); + }); + + return function (_x5, _x6) { + return _ref22.apply(this, arguments); + }; + })()); + } + + let currentStep = 0; + for (var _iterator11 = steps, _isArray11 = Array.isArray(_iterator11), _i11 = 0, _iterator11 = _isArray11 ? _iterator11 : _iterator11[Symbol.iterator]();;) { + var _ref23; + + if (_isArray11) { + if (_i11 >= _iterator11.length) break; + _ref23 = _iterator11[_i11++]; + } else { + _i11 = _iterator11.next(); + if (_i11.done) break; + _ref23 = _i11.value; + } + + const step = _ref23; + + const stepResult = yield step(++currentStep, steps.length); + if (stepResult && stepResult.bailout) { + if (_this6.flags.audit) { + audit.summary(); + } + if (auditFoundProblems) { + _this6.reporter.warn(_this6.reporter.lang('auditRunAuditForDetails')); + } + _this6.maybeOutputUpdate(); + return flattenedTopLevelPatterns; + } + } + + // fin! + if (_this6.flags.audit) { + audit.summary(); + } + if (auditFoundProblems) { + _this6.reporter.warn(_this6.reporter.lang('auditRunAuditForDetails')); + } + yield _this6.saveLockfileAndIntegrity(topLevelPatterns, workspaceLayout); + yield _this6.persistChanges(); + _this6.maybeOutputUpdate(); + _this6.config.requestManager.clearCache(); + return flattenedTopLevelPatterns; + })(); + } + + checkCompatibility() { + var _this7 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + var _ref24 = yield _this7.fetchRequestFromCwd(); + + const manifest = _ref24.manifest; + + yield (_packageCompatibility || _load_packageCompatibility()).checkOne(manifest, _this7.config, _this7.flags.ignoreEngines); + })(); + } + + persistChanges() { + var _this8 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // get all the different registry manifests in this folder + const manifests = yield _this8.config.getRootManifests(); + + if (yield _this8.applyChanges(manifests)) { + yield _this8.config.saveRootManifests(manifests); + } + })(); + } + + applyChanges(manifests) { + let hasChanged = false; + + if (this.config.plugnplayPersist) { + const object = manifests.npm.object; + + + if (typeof object.installConfig !== 'object') { + object.installConfig = {}; + } + + if (this.config.plugnplayEnabled && object.installConfig.pnp !== true) { + object.installConfig.pnp = true; + hasChanged = true; + } else if (!this.config.plugnplayEnabled && typeof object.installConfig.pnp !== 'undefined') { + delete object.installConfig.pnp; + hasChanged = true; + } + + if (Object.keys(object.installConfig).length === 0) { + delete object.installConfig; + } + } + + return Promise.resolve(hasChanged); + } + + /** + * Check if we should run the cleaning step. + */ + + shouldClean() { + return (_fs || _load_fs()).exists(path.join(this.config.lockfileFolder, (_constants || _load_constants()).CLEAN_FILENAME)); + } + + /** + * TODO + */ + + flatten(patterns) { + var _this9 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + if (!_this9.flags.flat) { + return patterns; + } + + const flattenedPatterns = []; + + for (var _iterator12 = _this9.resolver.getAllDependencyNamesByLevelOrder(patterns), _isArray12 = Array.isArray(_iterator12), _i12 = 0, _iterator12 = _isArray12 ? _iterator12 : _iterator12[Symbol.iterator]();;) { + var _ref25; + + if (_isArray12) { + if (_i12 >= _iterator12.length) break; + _ref25 = _iterator12[_i12++]; + } else { + _i12 = _iterator12.next(); + if (_i12.done) break; + _ref25 = _i12.value; + } + + const name = _ref25; + + const infos = _this9.resolver.getAllInfoForPackageName(name).filter(function (manifest) { + const ref = manifest._reference; + invariant(ref, 'expected package reference'); + return !ref.ignore; + }); + + if (infos.length === 0) { + continue; + } + + if (infos.length === 1) { + // single version of this package + // take out a single pattern as multiple patterns may have resolved to this package + flattenedPatterns.push(_this9.resolver.patternsByPackage[name][0]); + continue; + } + + const options = infos.map(function (info) { + const ref = info._reference; + invariant(ref, 'expected reference'); + return { + // TODO `and is required by {PARENT}`, + name: _this9.reporter.lang('manualVersionResolutionOption', ref.patterns.join(', '), info.version), + + value: info.version + }; + }); + const versions = infos.map(function (info) { + return info.version; + }); + let version; + + const resolutionVersion = _this9.resolutions[name]; + if (resolutionVersion && versions.indexOf(resolutionVersion) >= 0) { + // use json `resolution` version + version = resolutionVersion; + } else { + version = yield _this9.reporter.select(_this9.reporter.lang('manualVersionResolution', name), _this9.reporter.lang('answer'), options); + _this9.resolutions[name] = version; + } + + flattenedPatterns.push(_this9.resolver.collapseAllVersionsOfPackage(name, version)); + } + + // save resolutions to their appropriate root manifest + if (Object.keys(_this9.resolutions).length) { + const manifests = yield _this9.config.getRootManifests(); + + for (const name in _this9.resolutions) { + const version = _this9.resolutions[name]; + + const patterns = _this9.resolver.patternsByPackage[name]; + if (!patterns) { + continue; + } + + let manifest; + for (var _iterator13 = patterns, _isArray13 = Array.isArray(_iterator13), _i13 = 0, _iterator13 = _isArray13 ? _iterator13 : _iterator13[Symbol.iterator]();;) { + var _ref26; + + if (_isArray13) { + if (_i13 >= _iterator13.length) break; + _ref26 = _iterator13[_i13++]; + } else { + _i13 = _iterator13.next(); + if (_i13.done) break; + _ref26 = _i13.value; + } + + const pattern = _ref26; + + manifest = _this9.resolver.getResolvedPattern(pattern); + if (manifest) { + break; + } + } + invariant(manifest, 'expected manifest'); + + const ref = manifest._reference; + invariant(ref, 'expected reference'); + + const object = manifests[ref.registry].object; + object.resolutions = object.resolutions || {}; + object.resolutions[name] = version; + } + + yield _this9.config.saveRootManifests(manifests); + } + + return flattenedPatterns; + })(); + } + + /** + * Remove offline tarballs that are no longer required + */ + + pruneOfflineMirror(lockfile) { + var _this10 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const mirror = _this10.config.getOfflineMirrorPath(); + if (!mirror) { + return; + } + + const requiredTarballs = new Set(); + for (const dependency in lockfile) { + const resolved = lockfile[dependency].resolved; + if (resolved) { + const basename = path.basename(resolved.split('#')[0]); + if (dependency[0] === '@' && basename[0] !== '@') { + requiredTarballs.add(`${dependency.split('/')[0]}-${basename}`); + } + requiredTarballs.add(basename); + } + } + + const mirrorFiles = yield (_fs || _load_fs()).walk(mirror); + for (var _iterator14 = mirrorFiles, _isArray14 = Array.isArray(_iterator14), _i14 = 0, _iterator14 = _isArray14 ? _iterator14 : _iterator14[Symbol.iterator]();;) { + var _ref27; + + if (_isArray14) { + if (_i14 >= _iterator14.length) break; + _ref27 = _iterator14[_i14++]; + } else { + _i14 = _iterator14.next(); + if (_i14.done) break; + _ref27 = _i14.value; + } + + const file = _ref27; + + const isTarball = path.extname(file.basename) === '.tgz'; + // if using experimental-pack-script-packages-in-mirror flag, don't unlink prebuilt packages + const hasPrebuiltPackage = file.relative.startsWith('prebuilt/'); + if (isTarball && !hasPrebuiltPackage && !requiredTarballs.has(file.basename)) { + yield (_fs || _load_fs()).unlink(file.absolute); + } + } + })(); + } + + /** + * Save updated integrity and lockfiles. + */ + + saveLockfileAndIntegrity(patterns, workspaceLayout) { + var _this11 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const resolvedPatterns = {}; + Object.keys(_this11.resolver.patterns).forEach(function (pattern) { + if (!workspaceLayout || !workspaceLayout.getManifestByPattern(pattern)) { + resolvedPatterns[pattern] = _this11.resolver.patterns[pattern]; + } + }); + + // TODO this code is duplicated in a few places, need a common way to filter out workspace patterns from lockfile + patterns = patterns.filter(function (p) { + return !workspaceLayout || !workspaceLayout.getManifestByPattern(p); + }); + + const lockfileBasedOnResolver = _this11.lockfile.getLockfile(resolvedPatterns); + + if (_this11.config.pruneOfflineMirror) { + yield _this11.pruneOfflineMirror(lockfileBasedOnResolver); + } + + // write integrity hash + if (!_this11.config.plugnplayEnabled) { + yield _this11.integrityChecker.save(patterns, lockfileBasedOnResolver, _this11.flags, workspaceLayout, _this11.scripts.getArtifacts()); + } + + // --no-lockfile or --pure-lockfile or --frozen-lockfile + if (_this11.flags.lockfile === false || _this11.flags.pureLockfile || _this11.flags.frozenLockfile) { + return; + } + + const lockFileHasAllPatterns = patterns.every(function (p) { + return _this11.lockfile.getLocked(p); + }); + const lockfilePatternsMatch = Object.keys(_this11.lockfile.cache || {}).every(function (p) { + return lockfileBasedOnResolver[p]; + }); + const resolverPatternsAreSameAsInLockfile = Object.keys(lockfileBasedOnResolver).every(function (pattern) { + const manifest = _this11.lockfile.getLocked(pattern); + return manifest && manifest.resolved === lockfileBasedOnResolver[pattern].resolved && deepEqual(manifest.prebuiltVariants, lockfileBasedOnResolver[pattern].prebuiltVariants); + }); + const integrityPatternsAreSameAsInLockfile = Object.keys(lockfileBasedOnResolver).every(function (pattern) { + const existingIntegrityInfo = lockfileBasedOnResolver[pattern].integrity; + if (!existingIntegrityInfo) { + // if this entry does not have an integrity, no need to re-write the lockfile because of it + return true; + } + const manifest = _this11.lockfile.getLocked(pattern); + if (manifest && manifest.integrity) { + const manifestIntegrity = ssri.stringify(manifest.integrity); + return manifestIntegrity === existingIntegrityInfo; + } + return false; + }); + + // remove command is followed by install with force, lockfile will be rewritten in any case then + if (!_this11.flags.force && _this11.lockfile.parseResultType === 'success' && lockFileHasAllPatterns && lockfilePatternsMatch && resolverPatternsAreSameAsInLockfile && integrityPatternsAreSameAsInLockfile && patterns.length) { + return; + } + + // build lockfile location + const loc = path.join(_this11.config.lockfileFolder, (_constants || _load_constants()).LOCKFILE_FILENAME); + + // write lockfile + const lockSource = (0, (_lockfile2 || _load_lockfile2()).stringify)(lockfileBasedOnResolver, false, _this11.config.enableLockfileVersions); + yield (_fs || _load_fs()).writeFilePreservingEol(loc, lockSource); + + _this11._logSuccessSaveLockfile(); + })(); + } + + _logSuccessSaveLockfile() { + this.reporter.success(this.reporter.lang('savedLockfile')); + } + + /** + * Load the dependency graph of the current install. Only does package resolving and wont write to the cwd. + */ + hydrate(ignoreUnusedPatterns) { + var _this12 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const request = yield _this12.fetchRequestFromCwd([], ignoreUnusedPatterns); + const depRequests = request.requests, + rawPatterns = request.patterns, + ignorePatterns = request.ignorePatterns, + workspaceLayout = request.workspaceLayout; + + + yield _this12.resolver.init(depRequests, { + isFlat: _this12.flags.flat, + isFrozen: _this12.flags.frozenLockfile, + workspaceLayout + }); + yield _this12.flatten(rawPatterns); + _this12.markIgnored(ignorePatterns); + + // fetch packages, should hit cache most of the time + const manifests = yield (_packageFetcher || _load_packageFetcher()).fetch(_this12.resolver.getManifests(), _this12.config); + _this12.resolver.updateManifests(manifests); + yield (_packageCompatibility || _load_packageCompatibility()).check(_this12.resolver.getManifests(), _this12.config, _this12.flags.ignoreEngines); + + // expand minimal manifests + for (var _iterator15 = _this12.resolver.getManifests(), _isArray15 = Array.isArray(_iterator15), _i15 = 0, _iterator15 = _isArray15 ? _iterator15 : _iterator15[Symbol.iterator]();;) { + var _ref28; + + if (_isArray15) { + if (_i15 >= _iterator15.length) break; + _ref28 = _iterator15[_i15++]; + } else { + _i15 = _iterator15.next(); + if (_i15.done) break; + _ref28 = _i15.value; + } + + const manifest = _ref28; + + const ref = manifest._reference; + invariant(ref, 'expected reference'); + const type = ref.remote.type; + // link specifier won't ever hit cache + + let loc = ''; + if (type === 'link') { + continue; + } else if (type === 'workspace') { + if (!ref.remote.reference) { + continue; + } + loc = ref.remote.reference; + } else { + loc = _this12.config.generateModuleCachePath(ref); + } + const newPkg = yield _this12.config.readManifest(loc); + yield _this12.resolver.updateManifest(ref, newPkg); + } + + return request; + })(); + } + + /** + * Check for updates every day and output a nag message if there's a newer version. + */ + + checkUpdate() { + if (this.config.nonInteractive) { + // don't show upgrade dialog on CI or non-TTY terminals + return; + } + + // don't check if disabled + if (this.config.getOption('disable-self-update-check')) { + return; + } + + // only check for updates once a day + const lastUpdateCheck = Number(this.config.getOption('lastUpdateCheck')) || 0; + if (lastUpdateCheck && Date.now() - lastUpdateCheck < ONE_DAY) { + return; + } + + // don't bug for updates on tagged releases + if ((_yarnVersion || _load_yarnVersion()).version.indexOf('-') >= 0) { + return; + } + + this._checkUpdate().catch(() => { + // swallow errors + }); + } + + _checkUpdate() { + var _this13 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + let latestVersion = yield _this13.config.requestManager.request({ + url: (_constants || _load_constants()).SELF_UPDATE_VERSION_URL + }); + invariant(typeof latestVersion === 'string', 'expected string'); + latestVersion = latestVersion.trim(); + if (!semver.valid(latestVersion)) { + return; + } + + // ensure we only check for updates periodically + _this13.config.registries.yarn.saveHomeConfig({ + lastUpdateCheck: Date.now() + }); + + if (semver.gt(latestVersion, (_yarnVersion || _load_yarnVersion()).version)) { + const installationMethod = yield (0, (_yarnVersion || _load_yarnVersion()).getInstallationMethod)(); + _this13.maybeOutputUpdate = function () { + _this13.reporter.warn(_this13.reporter.lang('yarnOutdated', latestVersion, (_yarnVersion || _load_yarnVersion()).version)); + + const command = getUpdateCommand(installationMethod); + if (command) { + _this13.reporter.info(_this13.reporter.lang('yarnOutdatedCommand')); + _this13.reporter.command(command); + } else { + const installer = getUpdateInstaller(installationMethod); + if (installer) { + _this13.reporter.info(_this13.reporter.lang('yarnOutdatedInstaller', installer)); + } + } + }; + } + })(); + } + + /** + * Method to override with a possible upgrade message. + */ + + maybeOutputUpdate() {} +} + +exports.Install = Install; +function hasWrapper(commander, args) { + return true; +} + +function setFlags(commander) { + commander.description('Yarn install is used to install all dependencies for a project.'); + commander.usage('install [flags]'); + commander.option('-A, --audit', 'Run vulnerability audit on installed packages'); + commander.option('-g, --global', 'DEPRECATED'); + commander.option('-S, --save', 'DEPRECATED - save package to your `dependencies`'); + commander.option('-D, --save-dev', 'DEPRECATED - save package to your `devDependencies`'); + commander.option('-P, --save-peer', 'DEPRECATED - save package to your `peerDependencies`'); + commander.option('-O, --save-optional', 'DEPRECATED - save package to your `optionalDependencies`'); + commander.option('-E, --save-exact', 'DEPRECATED'); + commander.option('-T, --save-tilde', 'DEPRECATED'); +} + +/***/ }), +/* 35 */ +/***/ (function(module, exports, __webpack_require__) { + +var isObject = __webpack_require__(52); +module.exports = function (it) { + if (!isObject(it)) throw TypeError(it + ' is not an object!'); + return it; +}; + + +/***/ }), +/* 36 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return SubjectSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Subject; }); +/* unused harmony export AnonymousSubject */ +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_tslib__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Observable__ = __webpack_require__(11); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__Subscriber__ = __webpack_require__(7); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__Subscription__ = __webpack_require__(25); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__ = __webpack_require__(189); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__SubjectSubscription__ = __webpack_require__(422); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__internal_symbol_rxSubscriber__ = __webpack_require__(321); +/** PURE_IMPORTS_START tslib,_Observable,_Subscriber,_Subscription,_util_ObjectUnsubscribedError,_SubjectSubscription,_internal_symbol_rxSubscriber PURE_IMPORTS_END */ + + + + + + + +var SubjectSubscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](SubjectSubscriber, _super); + function SubjectSubscriber(destination) { + var _this = _super.call(this, destination) || this; + _this.destination = destination; + return _this; + } + return SubjectSubscriber; +}(__WEBPACK_IMPORTED_MODULE_2__Subscriber__["a" /* Subscriber */])); + +var Subject = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](Subject, _super); + function Subject() { + var _this = _super.call(this) || this; + _this.observers = []; + _this.closed = false; + _this.isStopped = false; + _this.hasError = false; + _this.thrownError = null; + return _this; + } + Subject.prototype[__WEBPACK_IMPORTED_MODULE_6__internal_symbol_rxSubscriber__["a" /* rxSubscriber */]] = function () { + return new SubjectSubscriber(this); + }; + Subject.prototype.lift = function (operator) { + var subject = new AnonymousSubject(this, this); + subject.operator = operator; + return subject; + }; + Subject.prototype.next = function (value) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + if (!this.isStopped) { + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].next(value); + } + } + }; + Subject.prototype.error = function (err) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + this.hasError = true; + this.thrownError = err; + this.isStopped = true; + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].error(err); + } + this.observers.length = 0; + }; + Subject.prototype.complete = function () { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + this.isStopped = true; + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].complete(); + } + this.observers.length = 0; + }; + Subject.prototype.unsubscribe = function () { + this.isStopped = true; + this.closed = true; + this.observers = null; + }; + Subject.prototype._trySubscribe = function (subscriber) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + else { + return _super.prototype._trySubscribe.call(this, subscriber); + } + }; + Subject.prototype._subscribe = function (subscriber) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + else if (this.hasError) { + subscriber.error(this.thrownError); + return __WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */].EMPTY; + } + else if (this.isStopped) { + subscriber.complete(); + return __WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */].EMPTY; + } + else { + this.observers.push(subscriber); + return new __WEBPACK_IMPORTED_MODULE_5__SubjectSubscription__["a" /* SubjectSubscription */](this, subscriber); + } + }; + Subject.prototype.asObservable = function () { + var observable = new __WEBPACK_IMPORTED_MODULE_1__Observable__["a" /* Observable */](); + observable.source = this; + return observable; + }; + Subject.create = function (destination, source) { + return new AnonymousSubject(destination, source); + }; + return Subject; +}(__WEBPACK_IMPORTED_MODULE_1__Observable__["a" /* Observable */])); + +var AnonymousSubject = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](AnonymousSubject, _super); + function AnonymousSubject(destination, source) { + var _this = _super.call(this) || this; + _this.destination = destination; + _this.source = source; + return _this; + } + AnonymousSubject.prototype.next = function (value) { + var destination = this.destination; + if (destination && destination.next) { + destination.next(value); + } + }; + AnonymousSubject.prototype.error = function (err) { + var destination = this.destination; + if (destination && destination.error) { + this.destination.error(err); + } + }; + AnonymousSubject.prototype.complete = function () { + var destination = this.destination; + if (destination && destination.complete) { + this.destination.complete(); + } + }; + AnonymousSubject.prototype._subscribe = function (subscriber) { + var source = this.source; + if (source) { + return this.source.subscribe(subscriber); + } + else { + return __WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */].EMPTY; + } + }; + return AnonymousSubject; +}(Subject)); + +//# sourceMappingURL=Subject.js.map + + +/***/ }), +/* 37 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.normalizePattern = normalizePattern; + +/** + * Explode and normalize a pattern into its name and range. + */ + +function normalizePattern(pattern) { + let hasVersion = false; + let range = 'latest'; + let name = pattern; + + // if we're a scope then remove the @ and add it back later + let isScoped = false; + if (name[0] === '@') { + isScoped = true; + name = name.slice(1); + } + + // take first part as the name + const parts = name.split('@'); + if (parts.length > 1) { + name = parts.shift(); + range = parts.join('@'); + + if (range) { + hasVersion = true; + } else { + range = '*'; + } + } + + // add back @ scope suffix + if (isScoped) { + name = `@${name}`; + } + + return { name, range, hasVersion }; +} + +/***/ }), +/* 38 */ +/***/ (function(module, exports, __webpack_require__) { + +/* WEBPACK VAR INJECTION */(function(module) {var __WEBPACK_AMD_DEFINE_RESULT__;/** + * @license + * Lodash + * Copyright JS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ +;(function() { + + /** Used as a safe reference for `undefined` in pre-ES5 environments. */ + var undefined; + + /** Used as the semantic version number. */ + var VERSION = '4.17.10'; + + /** Used as the size to enable large array optimizations. */ + var LARGE_ARRAY_SIZE = 200; + + /** Error message constants. */ + var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.', + FUNC_ERROR_TEXT = 'Expected a function'; + + /** Used to stand-in for `undefined` hash values. */ + var HASH_UNDEFINED = '__lodash_hash_undefined__'; + + /** Used as the maximum memoize cache size. */ + var MAX_MEMOIZE_SIZE = 500; + + /** Used as the internal argument placeholder. */ + var PLACEHOLDER = '__lodash_placeholder__'; + + /** Used to compose bitmasks for cloning. */ + var CLONE_DEEP_FLAG = 1, + CLONE_FLAT_FLAG = 2, + CLONE_SYMBOLS_FLAG = 4; + + /** Used to compose bitmasks for value comparisons. */ + var COMPARE_PARTIAL_FLAG = 1, + COMPARE_UNORDERED_FLAG = 2; + + /** Used to compose bitmasks for function metadata. */ + var WRAP_BIND_FLAG = 1, + WRAP_BIND_KEY_FLAG = 2, + WRAP_CURRY_BOUND_FLAG = 4, + WRAP_CURRY_FLAG = 8, + WRAP_CURRY_RIGHT_FLAG = 16, + WRAP_PARTIAL_FLAG = 32, + WRAP_PARTIAL_RIGHT_FLAG = 64, + WRAP_ARY_FLAG = 128, + WRAP_REARG_FLAG = 256, + WRAP_FLIP_FLAG = 512; + + /** Used as default options for `_.truncate`. */ + var DEFAULT_TRUNC_LENGTH = 30, + DEFAULT_TRUNC_OMISSION = '...'; + + /** Used to detect hot functions by number of calls within a span of milliseconds. */ + var HOT_COUNT = 800, + HOT_SPAN = 16; + + /** Used to indicate the type of lazy iteratees. */ + var LAZY_FILTER_FLAG = 1, + LAZY_MAP_FLAG = 2, + LAZY_WHILE_FLAG = 3; + + /** Used as references for various `Number` constants. */ + var INFINITY = 1 / 0, + MAX_SAFE_INTEGER = 9007199254740991, + MAX_INTEGER = 1.7976931348623157e+308, + NAN = 0 / 0; + + /** Used as references for the maximum length and index of an array. */ + var MAX_ARRAY_LENGTH = 4294967295, + MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1, + HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1; + + /** Used to associate wrap methods with their bit flags. */ + var wrapFlags = [ + ['ary', WRAP_ARY_FLAG], + ['bind', WRAP_BIND_FLAG], + ['bindKey', WRAP_BIND_KEY_FLAG], + ['curry', WRAP_CURRY_FLAG], + ['curryRight', WRAP_CURRY_RIGHT_FLAG], + ['flip', WRAP_FLIP_FLAG], + ['partial', WRAP_PARTIAL_FLAG], + ['partialRight', WRAP_PARTIAL_RIGHT_FLAG], + ['rearg', WRAP_REARG_FLAG] + ]; + + /** `Object#toString` result references. */ + var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + asyncTag = '[object AsyncFunction]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + domExcTag = '[object DOMException]', + errorTag = '[object Error]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + mapTag = '[object Map]', + numberTag = '[object Number]', + nullTag = '[object Null]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + proxyTag = '[object Proxy]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + symbolTag = '[object Symbol]', + undefinedTag = '[object Undefined]', + weakMapTag = '[object WeakMap]', + weakSetTag = '[object WeakSet]'; + + var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + + /** Used to match empty string literals in compiled template source. */ + var reEmptyStringLeading = /\b__p \+= '';/g, + reEmptyStringMiddle = /\b(__p \+=) '' \+/g, + reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; + + /** Used to match HTML entities and HTML characters. */ + var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g, + reUnescapedHtml = /[&<>"']/g, + reHasEscapedHtml = RegExp(reEscapedHtml.source), + reHasUnescapedHtml = RegExp(reUnescapedHtml.source); + + /** Used to match template delimiters. */ + var reEscape = /<%-([\s\S]+?)%>/g, + reEvaluate = /<%([\s\S]+?)%>/g, + reInterpolate = /<%=([\s\S]+?)%>/g; + + /** Used to match property names within property paths. */ + var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, + reIsPlainProp = /^\w*$/, + rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; + + /** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ + var reRegExpChar = /[\\^$.*+?()[\]{}|]/g, + reHasRegExpChar = RegExp(reRegExpChar.source); + + /** Used to match leading and trailing whitespace. */ + var reTrim = /^\s+|\s+$/g, + reTrimStart = /^\s+/, + reTrimEnd = /\s+$/; + + /** Used to match wrap detail comments. */ + var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/, + reWrapDetails = /\{\n\/\* \[wrapped with (.+)\] \*/, + reSplitDetails = /,? & /; + + /** Used to match words composed of alphanumeric characters. */ + var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; + + /** Used to match backslashes in property paths. */ + var reEscapeChar = /\\(\\)?/g; + + /** + * Used to match + * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components). + */ + var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; + + /** Used to match `RegExp` flags from their coerced string values. */ + var reFlags = /\w*$/; + + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + + /** Used to detect binary string values. */ + var reIsBinary = /^0b[01]+$/i; + + /** Used to detect host constructors (Safari). */ + var reIsHostCtor = /^\[object .+?Constructor\]$/; + + /** Used to detect octal string values. */ + var reIsOctal = /^0o[0-7]+$/i; + + /** Used to detect unsigned integer values. */ + var reIsUint = /^(?:0|[1-9]\d*)$/; + + /** Used to match Latin Unicode letters (excluding mathematical operators). */ + var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; + + /** Used to ensure capturing order of template delimiters. */ + var reNoMatch = /($^)/; + + /** Used to match unescaped characters in compiled string literals. */ + var reUnescapedString = /['\n\r\u2028\u2029\\]/g; + + /** Used to compose unicode character classes. */ + var rsAstralRange = '\\ud800-\\udfff', + rsComboMarksRange = '\\u0300-\\u036f', + reComboHalfMarksRange = '\\ufe20-\\ufe2f', + rsComboSymbolsRange = '\\u20d0-\\u20ff', + rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange, + rsDingbatRange = '\\u2700-\\u27bf', + rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff', + rsMathOpRange = '\\xac\\xb1\\xd7\\xf7', + rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf', + rsPunctuationRange = '\\u2000-\\u206f', + rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', + rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde', + rsVarRange = '\\ufe0e\\ufe0f', + rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange; + + /** Used to compose unicode capture groups. */ + var rsApos = "['\u2019]", + rsAstral = '[' + rsAstralRange + ']', + rsBreak = '[' + rsBreakRange + ']', + rsCombo = '[' + rsComboRange + ']', + rsDigits = '\\d+', + rsDingbat = '[' + rsDingbatRange + ']', + rsLower = '[' + rsLowerRange + ']', + rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']', + rsFitz = '\\ud83c[\\udffb-\\udfff]', + rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')', + rsNonAstral = '[^' + rsAstralRange + ']', + rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', + rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', + rsUpper = '[' + rsUpperRange + ']', + rsZWJ = '\\u200d'; + + /** Used to compose unicode regexes. */ + var rsMiscLower = '(?:' + rsLower + '|' + rsMisc + ')', + rsMiscUpper = '(?:' + rsUpper + '|' + rsMisc + ')', + rsOptContrLower = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?', + rsOptContrUpper = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?', + reOptMod = rsModifier + '?', + rsOptVar = '[' + rsVarRange + ']?', + rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', + rsOrdLower = '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])', + rsOrdUpper = '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])', + rsSeq = rsOptVar + reOptMod + rsOptJoin, + rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq, + rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; + + /** Used to match apostrophes. */ + var reApos = RegExp(rsApos, 'g'); + + /** + * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and + * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). + */ + var reComboMark = RegExp(rsCombo, 'g'); + + /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ + var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); + + /** Used to match complex or compound words. */ + var reUnicodeWord = RegExp([ + rsUpper + '?' + rsLower + '+' + rsOptContrLower + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')', + rsMiscUpper + '+' + rsOptContrUpper + '(?=' + [rsBreak, rsUpper + rsMiscLower, '$'].join('|') + ')', + rsUpper + '?' + rsMiscLower + '+' + rsOptContrLower, + rsUpper + '+' + rsOptContrUpper, + rsOrdUpper, + rsOrdLower, + rsDigits, + rsEmoji + ].join('|'), 'g'); + + /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ + var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']'); + + /** Used to detect strings that need a more robust regexp to match words. */ + var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; + + /** Used to assign default `context` object properties. */ + var contextProps = [ + 'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array', + 'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object', + 'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array', + 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', + '_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout' + ]; + + /** Used to make template sourceURLs easier to identify. */ + var templateCounter = -1; + + /** Used to identify `toStringTag` values of typed arrays. */ + var typedArrayTags = {}; + typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = + typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = + typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = + typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = + typedArrayTags[uint32Tag] = true; + typedArrayTags[argsTag] = typedArrayTags[arrayTag] = + typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = + typedArrayTags[dataViewTag] = typedArrayTags[dateTag] = + typedArrayTags[errorTag] = typedArrayTags[funcTag] = + typedArrayTags[mapTag] = typedArrayTags[numberTag] = + typedArrayTags[objectTag] = typedArrayTags[regexpTag] = + typedArrayTags[setTag] = typedArrayTags[stringTag] = + typedArrayTags[weakMapTag] = false; + + /** Used to identify `toStringTag` values supported by `_.clone`. */ + var cloneableTags = {}; + cloneableTags[argsTag] = cloneableTags[arrayTag] = + cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = + cloneableTags[boolTag] = cloneableTags[dateTag] = + cloneableTags[float32Tag] = cloneableTags[float64Tag] = + cloneableTags[int8Tag] = cloneableTags[int16Tag] = + cloneableTags[int32Tag] = cloneableTags[mapTag] = + cloneableTags[numberTag] = cloneableTags[objectTag] = + cloneableTags[regexpTag] = cloneableTags[setTag] = + cloneableTags[stringTag] = cloneableTags[symbolTag] = + cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = + cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; + cloneableTags[errorTag] = cloneableTags[funcTag] = + cloneableTags[weakMapTag] = false; + + /** Used to map Latin Unicode letters to basic Latin letters. */ + var deburredLetters = { + // Latin-1 Supplement block. + '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', + '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', + '\xc7': 'C', '\xe7': 'c', + '\xd0': 'D', '\xf0': 'd', + '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', + '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', + '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', + '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', + '\xd1': 'N', '\xf1': 'n', + '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', + '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', + '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', + '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', + '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', + '\xc6': 'Ae', '\xe6': 'ae', + '\xde': 'Th', '\xfe': 'th', + '\xdf': 'ss', + // Latin Extended-A block. + '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', + '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', + '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', + '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', + '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', + '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', + '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', + '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', + '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', + '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', + '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', + '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', + '\u0134': 'J', '\u0135': 'j', + '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', + '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', + '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', + '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', + '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', + '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', + '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', + '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', + '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', + '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', + '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', + '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', + '\u0163': 't', '\u0165': 't', '\u0167': 't', + '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', + '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', + '\u0174': 'W', '\u0175': 'w', + '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', + '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', + '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', + '\u0132': 'IJ', '\u0133': 'ij', + '\u0152': 'Oe', '\u0153': 'oe', + '\u0149': "'n", '\u017f': 's' + }; + + /** Used to map characters to HTML entities. */ + var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + /** Used to map HTML entities to characters. */ + var htmlUnescapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'" + }; + + /** Used to escape characters for inclusion in compiled string literals. */ + var stringEscapes = { + '\\': '\\', + "'": "'", + '\n': 'n', + '\r': 'r', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + /** Built-in method references without a dependency on `root`. */ + var freeParseFloat = parseFloat, + freeParseInt = parseInt; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + + /** Detect free variable `exports`. */ + var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; + + /** Detect free variable `module`. */ + var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; + + /** Detect the popular CommonJS extension `module.exports`. */ + var moduleExports = freeModule && freeModule.exports === freeExports; + + /** Detect free variable `process` from Node.js. */ + var freeProcess = moduleExports && freeGlobal.process; + + /** Used to access faster Node.js helpers. */ + var nodeUtil = (function() { + try { + // Use `util.types` for Node.js 10+. + var types = freeModule && freeModule.require && freeModule.require('util').types; + + if (types) { + return types; + } + + // Legacy `process.binding('util')` for Node.js < 10. + return freeProcess && freeProcess.binding && freeProcess.binding('util'); + } catch (e) {} + }()); + + /* Node.js helper references. */ + var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer, + nodeIsDate = nodeUtil && nodeUtil.isDate, + nodeIsMap = nodeUtil && nodeUtil.isMap, + nodeIsRegExp = nodeUtil && nodeUtil.isRegExp, + nodeIsSet = nodeUtil && nodeUtil.isSet, + nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray; + + /*--------------------------------------------------------------------------*/ + + /** + * A faster alternative to `Function#apply`, this function invokes `func` + * with the `this` binding of `thisArg` and the arguments of `args`. + * + * @private + * @param {Function} func The function to invoke. + * @param {*} thisArg The `this` binding of `func`. + * @param {Array} args The arguments to invoke `func` with. + * @returns {*} Returns the result of `func`. + */ + function apply(func, thisArg, args) { + switch (args.length) { + case 0: return func.call(thisArg); + case 1: return func.call(thisArg, args[0]); + case 2: return func.call(thisArg, args[0], args[1]); + case 3: return func.call(thisArg, args[0], args[1], args[2]); + } + return func.apply(thisArg, args); + } + + /** + * A specialized version of `baseAggregator` for arrays. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform keys. + * @param {Object} accumulator The initial aggregated object. + * @returns {Function} Returns `accumulator`. + */ + function arrayAggregator(array, setter, iteratee, accumulator) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + var value = array[index]; + setter(accumulator, value, iteratee(value), array); + } + return accumulator; + } + + /** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.forEachRight` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEachRight(array, iteratee) { + var length = array == null ? 0 : array.length; + + while (length--) { + if (iteratee(array[length], length, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.every` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false`. + */ + function arrayEvery(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (!predicate(array[index], index, array)) { + return false; + } + } + return true; + } + + /** + * A specialized version of `_.filter` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ + function arrayFilter(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result[resIndex++] = value; + } + } + return result; + } + + /** + * A specialized version of `_.includes` for arrays without support for + * specifying an index to search from. + * + * @private + * @param {Array} [array] The array to inspect. + * @param {*} target The value to search for. + * @returns {boolean} Returns `true` if `target` is found, else `false`. + */ + function arrayIncludes(array, value) { + var length = array == null ? 0 : array.length; + return !!length && baseIndexOf(array, value, 0) > -1; + } + + /** + * This function is like `arrayIncludes` except that it accepts a comparator. + * + * @private + * @param {Array} [array] The array to inspect. + * @param {*} target The value to search for. + * @param {Function} comparator The comparator invoked per element. + * @returns {boolean} Returns `true` if `target` is found, else `false`. + */ + function arrayIncludesWith(array, value, comparator) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (comparator(value, array[index])) { + return true; + } + } + return false; + } + + /** + * A specialized version of `_.map` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function arrayMap(array, iteratee) { + var index = -1, + length = array == null ? 0 : array.length, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; + } + + /** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ + function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; + } + + /** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array == null ? 0 : array.length; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * A specialized version of `_.reduceRight` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the last element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduceRight(array, iteratee, accumulator, initAccum) { + var length = array == null ? 0 : array.length; + if (initAccum && length) { + accumulator = array[--length]; + } + while (length--) { + accumulator = iteratee(accumulator, array[length], length, array); + } + return accumulator; + } + + /** + * A specialized version of `_.some` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + */ + function arraySome(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (predicate(array[index], index, array)) { + return true; + } + } + return false; + } + + /** + * Gets the size of an ASCII `string`. + * + * @private + * @param {string} string The string inspect. + * @returns {number} Returns the string size. + */ + var asciiSize = baseProperty('length'); + + /** + * Converts an ASCII `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function asciiToArray(string) { + return string.split(''); + } + + /** + * Splits an ASCII `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function asciiWords(string) { + return string.match(reAsciiWord) || []; + } + + /** + * The base implementation of methods like `_.findKey` and `_.findLastKey`, + * without support for iteratee shorthands, which iterates over `collection` + * using `eachFunc`. + * + * @private + * @param {Array|Object} collection The collection to inspect. + * @param {Function} predicate The function invoked per iteration. + * @param {Function} eachFunc The function to iterate over `collection`. + * @returns {*} Returns the found element or its key, else `undefined`. + */ + function baseFindKey(collection, predicate, eachFunc) { + var result; + eachFunc(collection, function(value, key, collection) { + if (predicate(value, key, collection)) { + result = key; + return false; + } + }); + return result; + } + + /** + * The base implementation of `_.findIndex` and `_.findLastIndex` without + * support for iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} predicate The function invoked per iteration. + * @param {number} fromIndex The index to search from. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseFindIndex(array, predicate, fromIndex, fromRight) { + var length = array.length, + index = fromIndex + (fromRight ? 1 : -1); + + while ((fromRight ? index-- : ++index < length)) { + if (predicate(array[index], index, array)) { + return index; + } + } + return -1; + } + + /** + * The base implementation of `_.indexOf` without `fromIndex` bounds checks. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseIndexOf(array, value, fromIndex) { + return value === value + ? strictIndexOf(array, value, fromIndex) + : baseFindIndex(array, baseIsNaN, fromIndex); + } + + /** + * This function is like `baseIndexOf` except that it accepts a comparator. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @param {Function} comparator The comparator invoked per element. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseIndexOfWith(array, value, fromIndex, comparator) { + var index = fromIndex - 1, + length = array.length; + + while (++index < length) { + if (comparator(array[index], value)) { + return index; + } + } + return -1; + } + + /** + * The base implementation of `_.isNaN` without support for number objects. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. + */ + function baseIsNaN(value) { + return value !== value; + } + + /** + * The base implementation of `_.mean` and `_.meanBy` without support for + * iteratee shorthands. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {number} Returns the mean. + */ + function baseMean(array, iteratee) { + var length = array == null ? 0 : array.length; + return length ? (baseSum(array, iteratee) / length) : NAN; + } + + /** + * The base implementation of `_.property` without support for deep paths. + * + * @private + * @param {string} key The key of the property to get. + * @returns {Function} Returns the new accessor function. + */ + function baseProperty(key) { + return function(object) { + return object == null ? undefined : object[key]; + }; + } + + /** + * The base implementation of `_.propertyOf` without support for deep paths. + * + * @private + * @param {Object} object The object to query. + * @returns {Function} Returns the new accessor function. + */ + function basePropertyOf(object) { + return function(key) { + return object == null ? undefined : object[key]; + }; + } + + /** + * The base implementation of `_.reduce` and `_.reduceRight`, without support + * for iteratee shorthands, which iterates over `collection` using `eachFunc`. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} accumulator The initial value. + * @param {boolean} initAccum Specify using the first or last element of + * `collection` as the initial value. + * @param {Function} eachFunc The function to iterate over `collection`. + * @returns {*} Returns the accumulated value. + */ + function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) { + eachFunc(collection, function(value, index, collection) { + accumulator = initAccum + ? (initAccum = false, value) + : iteratee(accumulator, value, index, collection); + }); + return accumulator; + } + + /** + * The base implementation of `_.sortBy` which uses `comparer` to define the + * sort order of `array` and replaces criteria objects with their corresponding + * values. + * + * @private + * @param {Array} array The array to sort. + * @param {Function} comparer The function to define sort order. + * @returns {Array} Returns `array`. + */ + function baseSortBy(array, comparer) { + var length = array.length; + + array.sort(comparer); + while (length--) { + array[length] = array[length].value; + } + return array; + } + + /** + * The base implementation of `_.sum` and `_.sumBy` without support for + * iteratee shorthands. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {number} Returns the sum. + */ + function baseSum(array, iteratee) { + var result, + index = -1, + length = array.length; + + while (++index < length) { + var current = iteratee(array[index]); + if (current !== undefined) { + result = result === undefined ? current : (result + current); + } + } + return result; + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array + * of key-value pairs for `object` corresponding to the property names of `props`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} props The property names to get values for. + * @returns {Object} Returns the key-value pairs. + */ + function baseToPairs(object, props) { + return arrayMap(props, function(key) { + return [key, object[key]]; + }); + } + + /** + * The base implementation of `_.unary` without support for storing metadata. + * + * @private + * @param {Function} func The function to cap arguments for. + * @returns {Function} Returns the new capped function. + */ + function baseUnary(func) { + return function(value) { + return func(value); + }; + } + + /** + * The base implementation of `_.values` and `_.valuesIn` which creates an + * array of `object` property values corresponding to the property names + * of `props`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} props The property names to get values for. + * @returns {Object} Returns the array of property values. + */ + function baseValues(object, props) { + return arrayMap(props, function(key) { + return object[key]; + }); + } + + /** + * Checks if a `cache` value for `key` exists. + * + * @private + * @param {Object} cache The cache to query. + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function cacheHas(cache, key) { + return cache.has(key); + } + + /** + * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol + * that is not found in the character symbols. + * + * @private + * @param {Array} strSymbols The string symbols to inspect. + * @param {Array} chrSymbols The character symbols to find. + * @returns {number} Returns the index of the first unmatched string symbol. + */ + function charsStartIndex(strSymbols, chrSymbols) { + var index = -1, + length = strSymbols.length; + + while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} + return index; + } + + /** + * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol + * that is not found in the character symbols. + * + * @private + * @param {Array} strSymbols The string symbols to inspect. + * @param {Array} chrSymbols The character symbols to find. + * @returns {number} Returns the index of the last unmatched string symbol. + */ + function charsEndIndex(strSymbols, chrSymbols) { + var index = strSymbols.length; + + while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} + return index; + } + + /** + * Gets the number of `placeholder` occurrences in `array`. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} placeholder The placeholder to search for. + * @returns {number} Returns the placeholder count. + */ + function countHolders(array, placeholder) { + var length = array.length, + result = 0; + + while (length--) { + if (array[length] === placeholder) { + ++result; + } + } + return result; + } + + /** + * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A + * letters to basic Latin letters. + * + * @private + * @param {string} letter The matched letter to deburr. + * @returns {string} Returns the deburred letter. + */ + var deburrLetter = basePropertyOf(deburredLetters); + + /** + * Used by `_.escape` to convert characters to HTML entities. + * + * @private + * @param {string} chr The matched character to escape. + * @returns {string} Returns the escaped character. + */ + var escapeHtmlChar = basePropertyOf(htmlEscapes); + + /** + * Used by `_.template` to escape characters for inclusion in compiled string literals. + * + * @private + * @param {string} chr The matched character to escape. + * @returns {string} Returns the escaped character. + */ + function escapeStringChar(chr) { + return '\\' + stringEscapes[chr]; + } + + /** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function getValue(object, key) { + return object == null ? undefined : object[key]; + } + + /** + * Checks if `string` contains Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a symbol is found, else `false`. + */ + function hasUnicode(string) { + return reHasUnicode.test(string); + } + + /** + * Checks if `string` contains a word composed of Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a word is found, else `false`. + */ + function hasUnicodeWord(string) { + return reHasUnicodeWord.test(string); + } + + /** + * Converts `iterator` to an array. + * + * @private + * @param {Object} iterator The iterator to convert. + * @returns {Array} Returns the converted array. + */ + function iteratorToArray(iterator) { + var data, + result = []; + + while (!(data = iterator.next()).done) { + result.push(data.value); + } + return result; + } + + /** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ + function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** + * Replaces all `placeholder` elements in `array` with an internal placeholder + * and returns an array of their indexes. + * + * @private + * @param {Array} array The array to modify. + * @param {*} placeholder The placeholder to replace. + * @returns {Array} Returns the new array of placeholder indexes. + */ + function replaceHolders(array, placeholder) { + var index = -1, + length = array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value === placeholder || value === PLACEHOLDER) { + array[index] = PLACEHOLDER; + result[resIndex++] = index; + } + } + return result; + } + + /** + * Gets the value at `key`, unless `key` is "__proto__". + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function safeGet(object, key) { + return key == '__proto__' + ? undefined + : object[key]; + } + + /** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ + function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; + } + + /** + * Converts `set` to its value-value pairs. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the value-value pairs. + */ + function setToPairs(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = [value, value]; + }); + return result; + } + + /** + * A specialized version of `_.indexOf` which performs strict equality + * comparisons of values, i.e. `===`. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function strictIndexOf(array, value, fromIndex) { + var index = fromIndex - 1, + length = array.length; + + while (++index < length) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * A specialized version of `_.lastIndexOf` which performs strict equality + * comparisons of values, i.e. `===`. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function strictLastIndexOf(array, value, fromIndex) { + var index = fromIndex + 1; + while (index--) { + if (array[index] === value) { + return index; + } + } + return index; + } + + /** + * Gets the number of symbols in `string`. + * + * @private + * @param {string} string The string to inspect. + * @returns {number} Returns the string size. + */ + function stringSize(string) { + return hasUnicode(string) + ? unicodeSize(string) + : asciiSize(string); + } + + /** + * Converts `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function stringToArray(string) { + return hasUnicode(string) + ? unicodeToArray(string) + : asciiToArray(string); + } + + /** + * Used by `_.unescape` to convert HTML entities to characters. + * + * @private + * @param {string} chr The matched character to unescape. + * @returns {string} Returns the unescaped character. + */ + var unescapeHtmlChar = basePropertyOf(htmlUnescapes); + + /** + * Gets the size of a Unicode `string`. + * + * @private + * @param {string} string The string inspect. + * @returns {number} Returns the string size. + */ + function unicodeSize(string) { + var result = reUnicode.lastIndex = 0; + while (reUnicode.test(string)) { + ++result; + } + return result; + } + + /** + * Converts a Unicode `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function unicodeToArray(string) { + return string.match(reUnicode) || []; + } + + /** + * Splits a Unicode `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function unicodeWords(string) { + return string.match(reUnicodeWord) || []; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Create a new pristine `lodash` function using the `context` object. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category Util + * @param {Object} [context=root] The context object. + * @returns {Function} Returns a new `lodash` function. + * @example + * + * _.mixin({ 'foo': _.constant('foo') }); + * + * var lodash = _.runInContext(); + * lodash.mixin({ 'bar': lodash.constant('bar') }); + * + * _.isFunction(_.foo); + * // => true + * _.isFunction(_.bar); + * // => false + * + * lodash.isFunction(lodash.foo); + * // => false + * lodash.isFunction(lodash.bar); + * // => true + * + * // Create a suped-up `defer` in Node.js. + * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer; + */ + var runInContext = (function runInContext(context) { + context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps)); + + /** Built-in constructor references. */ + var Array = context.Array, + Date = context.Date, + Error = context.Error, + Function = context.Function, + Math = context.Math, + Object = context.Object, + RegExp = context.RegExp, + String = context.String, + TypeError = context.TypeError; + + /** Used for built-in method references. */ + var arrayProto = Array.prototype, + funcProto = Function.prototype, + objectProto = Object.prototype; + + /** Used to detect overreaching core-js shims. */ + var coreJsData = context['__core-js_shared__']; + + /** Used to resolve the decompiled source of functions. */ + var funcToString = funcProto.toString; + + /** Used to check objects for own properties. */ + var hasOwnProperty = objectProto.hasOwnProperty; + + /** Used to generate unique IDs. */ + var idCounter = 0; + + /** Used to detect methods masquerading as native. */ + var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; + }()); + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var nativeObjectToString = objectProto.toString; + + /** Used to infer the `Object` constructor. */ + var objectCtorString = funcToString.call(Object); + + /** Used to restore the original `_` reference in `_.noConflict`. */ + var oldDash = root._; + + /** Used to detect if a method is native. */ + var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' + ); + + /** Built-in value references. */ + var Buffer = moduleExports ? context.Buffer : undefined, + Symbol = context.Symbol, + Uint8Array = context.Uint8Array, + allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined, + getPrototype = overArg(Object.getPrototypeOf, Object), + objectCreate = Object.create, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice, + spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined, + symIterator = Symbol ? Symbol.iterator : undefined, + symToStringTag = Symbol ? Symbol.toStringTag : undefined; + + var defineProperty = (function() { + try { + var func = getNative(Object, 'defineProperty'); + func({}, '', {}); + return func; + } catch (e) {} + }()); + + /** Mocked built-ins. */ + var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout, + ctxNow = Date && Date.now !== root.Date.now && Date.now, + ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeCeil = Math.ceil, + nativeFloor = Math.floor, + nativeGetSymbols = Object.getOwnPropertySymbols, + nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, + nativeIsFinite = context.isFinite, + nativeJoin = arrayProto.join, + nativeKeys = overArg(Object.keys, Object), + nativeMax = Math.max, + nativeMin = Math.min, + nativeNow = Date.now, + nativeParseInt = context.parseInt, + nativeRandom = Math.random, + nativeReverse = arrayProto.reverse; + + /* Built-in method references that are verified to be native. */ + var DataView = getNative(context, 'DataView'), + Map = getNative(context, 'Map'), + Promise = getNative(context, 'Promise'), + Set = getNative(context, 'Set'), + WeakMap = getNative(context, 'WeakMap'), + nativeCreate = getNative(Object, 'create'); + + /** Used to store function metadata. */ + var metaMap = WeakMap && new WeakMap; + + /** Used to lookup unminified function names. */ + var realNames = {}; + + /** Used to detect maps, sets, and weakmaps. */ + var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map), + promiseCtorString = toSource(Promise), + setCtorString = toSource(Set), + weakMapCtorString = toSource(WeakMap); + + /** Used to convert symbols to primitives and strings. */ + var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto ? symbolProto.valueOf : undefined, + symbolToString = symbolProto ? symbolProto.toString : undefined; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object which wraps `value` to enable implicit method + * chain sequences. Methods that operate on and return arrays, collections, + * and functions can be chained together. Methods that retrieve a single value + * or may return a primitive value will automatically end the chain sequence + * and return the unwrapped value. Otherwise, the value must be unwrapped + * with `_#value`. + * + * Explicit chain sequences, which must be unwrapped with `_#value`, may be + * enabled using `_.chain`. + * + * The execution of chained methods is lazy, that is, it's deferred until + * `_#value` is implicitly or explicitly called. + * + * Lazy evaluation allows several methods to support shortcut fusion. + * Shortcut fusion is an optimization to merge iteratee calls; this avoids + * the creation of intermediate arrays and can greatly reduce the number of + * iteratee executions. Sections of a chain sequence qualify for shortcut + * fusion if the section is applied to an array and iteratees accept only + * one argument. The heuristic for whether a section qualifies for shortcut + * fusion is subject to change. + * + * Chaining is supported in custom builds as long as the `_#value` method is + * directly or indirectly included in the build. + * + * In addition to lodash methods, wrappers have `Array` and `String` methods. + * + * The wrapper `Array` methods are: + * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift` + * + * The wrapper `String` methods are: + * `replace` and `split` + * + * The wrapper methods that support shortcut fusion are: + * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`, + * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`, + * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray` + * + * The chainable wrapper methods are: + * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`, + * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`, + * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`, + * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`, + * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`, + * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`, + * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`, + * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`, + * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`, + * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`, + * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`, + * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`, + * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`, + * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`, + * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`, + * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`, + * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`, + * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`, + * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`, + * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`, + * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`, + * `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`, + * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, + * `zipObject`, `zipObjectDeep`, and `zipWith` + * + * The wrapper methods that are **not** chainable by default are: + * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`, + * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`, + * `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`, + * `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`, + * `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`, + * `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`, + * `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`, + * `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`, + * `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`, + * `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`, + * `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`, + * `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`, + * `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`, + * `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`, + * `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`, + * `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`, + * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, + * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`, + * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`, + * `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`, + * `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`, + * `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`, + * `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`, + * `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`, + * `upperFirst`, `value`, and `words` + * + * @name _ + * @constructor + * @category Seq + * @param {*} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * function square(n) { + * return n * n; + * } + * + * var wrapped = _([1, 2, 3]); + * + * // Returns an unwrapped value. + * wrapped.reduce(_.add); + * // => 6 + * + * // Returns a wrapped value. + * var squares = wrapped.map(square); + * + * _.isArray(squares); + * // => false + * + * _.isArray(squares.value()); + * // => true + */ + function lodash(value) { + if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) { + if (value instanceof LodashWrapper) { + return value; + } + if (hasOwnProperty.call(value, '__wrapped__')) { + return wrapperClone(value); + } + } + return new LodashWrapper(value); + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} proto The object to inherit from. + * @returns {Object} Returns the new object. + */ + var baseCreate = (function() { + function object() {} + return function(proto) { + if (!isObject(proto)) { + return {}; + } + if (objectCreate) { + return objectCreate(proto); + } + object.prototype = proto; + var result = new object; + object.prototype = undefined; + return result; + }; + }()); + + /** + * The function whose prototype chain sequence wrappers inherit from. + * + * @private + */ + function baseLodash() { + // No operation performed. + } + + /** + * The base constructor for creating `lodash` wrapper objects. + * + * @private + * @param {*} value The value to wrap. + * @param {boolean} [chainAll] Enable explicit method chain sequences. + */ + function LodashWrapper(value, chainAll) { + this.__wrapped__ = value; + this.__actions__ = []; + this.__chain__ = !!chainAll; + this.__index__ = 0; + this.__values__ = undefined; + } + + /** + * By default, the template delimiters used by lodash are like those in + * embedded Ruby (ERB) as well as ES2015 template strings. Change the + * following template settings to use alternative delimiters. + * + * @static + * @memberOf _ + * @type {Object} + */ + lodash.templateSettings = { + + /** + * Used to detect `data` property values to be HTML-escaped. + * + * @memberOf _.templateSettings + * @type {RegExp} + */ + 'escape': reEscape, + + /** + * Used to detect code to be evaluated. + * + * @memberOf _.templateSettings + * @type {RegExp} + */ + 'evaluate': reEvaluate, + + /** + * Used to detect `data` property values to inject. + * + * @memberOf _.templateSettings + * @type {RegExp} + */ + 'interpolate': reInterpolate, + + /** + * Used to reference the data object in the template text. + * + * @memberOf _.templateSettings + * @type {string} + */ + 'variable': '', + + /** + * Used to import variables into the compiled template. + * + * @memberOf _.templateSettings + * @type {Object} + */ + 'imports': { + + /** + * A reference to the `lodash` function. + * + * @memberOf _.templateSettings.imports + * @type {Function} + */ + '_': lodash + } + }; + + // Ensure wrappers are instances of `baseLodash`. + lodash.prototype = baseLodash.prototype; + lodash.prototype.constructor = lodash; + + LodashWrapper.prototype = baseCreate(baseLodash.prototype); + LodashWrapper.prototype.constructor = LodashWrapper; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation. + * + * @private + * @constructor + * @param {*} value The value to wrap. + */ + function LazyWrapper(value) { + this.__wrapped__ = value; + this.__actions__ = []; + this.__dir__ = 1; + this.__filtered__ = false; + this.__iteratees__ = []; + this.__takeCount__ = MAX_ARRAY_LENGTH; + this.__views__ = []; + } + + /** + * Creates a clone of the lazy wrapper object. + * + * @private + * @name clone + * @memberOf LazyWrapper + * @returns {Object} Returns the cloned `LazyWrapper` object. + */ + function lazyClone() { + var result = new LazyWrapper(this.__wrapped__); + result.__actions__ = copyArray(this.__actions__); + result.__dir__ = this.__dir__; + result.__filtered__ = this.__filtered__; + result.__iteratees__ = copyArray(this.__iteratees__); + result.__takeCount__ = this.__takeCount__; + result.__views__ = copyArray(this.__views__); + return result; + } + + /** + * Reverses the direction of lazy iteration. + * + * @private + * @name reverse + * @memberOf LazyWrapper + * @returns {Object} Returns the new reversed `LazyWrapper` object. + */ + function lazyReverse() { + if (this.__filtered__) { + var result = new LazyWrapper(this); + result.__dir__ = -1; + result.__filtered__ = true; + } else { + result = this.clone(); + result.__dir__ *= -1; + } + return result; + } + + /** + * Extracts the unwrapped value from its lazy wrapper. + * + * @private + * @name value + * @memberOf LazyWrapper + * @returns {*} Returns the unwrapped value. + */ + function lazyValue() { + var array = this.__wrapped__.value(), + dir = this.__dir__, + isArr = isArray(array), + isRight = dir < 0, + arrLength = isArr ? array.length : 0, + view = getView(0, arrLength, this.__views__), + start = view.start, + end = view.end, + length = end - start, + index = isRight ? end : (start - 1), + iteratees = this.__iteratees__, + iterLength = iteratees.length, + resIndex = 0, + takeCount = nativeMin(length, this.__takeCount__); + + if (!isArr || (!isRight && arrLength == length && takeCount == length)) { + return baseWrapperValue(array, this.__actions__); + } + var result = []; + + outer: + while (length-- && resIndex < takeCount) { + index += dir; + + var iterIndex = -1, + value = array[index]; + + while (++iterIndex < iterLength) { + var data = iteratees[iterIndex], + iteratee = data.iteratee, + type = data.type, + computed = iteratee(value); + + if (type == LAZY_MAP_FLAG) { + value = computed; + } else if (!computed) { + if (type == LAZY_FILTER_FLAG) { + continue outer; + } else { + break outer; + } + } + } + result[resIndex++] = value; + } + return result; + } + + // Ensure `LazyWrapper` is an instance of `baseLodash`. + LazyWrapper.prototype = baseCreate(baseLodash.prototype); + LazyWrapper.prototype.constructor = LazyWrapper; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Hash(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ + function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + this.size = 0; + } + + /** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function hashDelete(key) { + var result = this.has(key) && delete this.__data__[key]; + this.size -= result ? 1 : 0; + return result; + } + + /** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; + } + + /** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function hashHas(key) { + var data = this.__data__; + return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); + } + + /** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ + function hashSet(key, value) { + var data = this.__data__; + this.size += this.has(key) ? 0 : 1; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; + } + + // Add methods to `Hash`. + Hash.prototype.clear = hashClear; + Hash.prototype['delete'] = hashDelete; + Hash.prototype.get = hashGet; + Hash.prototype.has = hashHas; + Hash.prototype.set = hashSet; + + /*------------------------------------------------------------------------*/ + + /** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function ListCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ + function listCacheClear() { + this.__data__ = []; + this.size = 0; + } + + /** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + --this.size; + return true; + } + + /** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; + } + + /** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; + } + + /** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ + function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + ++this.size; + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; + } + + // Add methods to `ListCache`. + ListCache.prototype.clear = listCacheClear; + ListCache.prototype['delete'] = listCacheDelete; + ListCache.prototype.get = listCacheGet; + ListCache.prototype.has = listCacheHas; + ListCache.prototype.set = listCacheSet; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function MapCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ + function mapCacheClear() { + this.size = 0; + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; + } + + /** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function mapCacheDelete(key) { + var result = getMapData(this, key)['delete'](key); + this.size -= result ? 1 : 0; + return result; + } + + /** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function mapCacheGet(key) { + return getMapData(this, key).get(key); + } + + /** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function mapCacheHas(key) { + return getMapData(this, key).has(key); + } + + /** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ + function mapCacheSet(key, value) { + var data = getMapData(this, key), + size = data.size; + + data.set(key, value); + this.size += data.size == size ? 0 : 1; + return this; + } + + // Add methods to `MapCache`. + MapCache.prototype.clear = mapCacheClear; + MapCache.prototype['delete'] = mapCacheDelete; + MapCache.prototype.get = mapCacheGet; + MapCache.prototype.has = mapCacheHas; + MapCache.prototype.set = mapCacheSet; + + /*------------------------------------------------------------------------*/ + + /** + * + * Creates an array cache object to store unique values. + * + * @private + * @constructor + * @param {Array} [values] The values to cache. + */ + function SetCache(values) { + var index = -1, + length = values == null ? 0 : values.length; + + this.__data__ = new MapCache; + while (++index < length) { + this.add(values[index]); + } + } + + /** + * Adds `value` to the array cache. + * + * @private + * @name add + * @memberOf SetCache + * @alias push + * @param {*} value The value to cache. + * @returns {Object} Returns the cache instance. + */ + function setCacheAdd(value) { + this.__data__.set(value, HASH_UNDEFINED); + return this; + } + + /** + * Checks if `value` is in the array cache. + * + * @private + * @name has + * @memberOf SetCache + * @param {*} value The value to search for. + * @returns {number} Returns `true` if `value` is found, else `false`. + */ + function setCacheHas(value) { + return this.__data__.has(value); + } + + // Add methods to `SetCache`. + SetCache.prototype.add = SetCache.prototype.push = setCacheAdd; + SetCache.prototype.has = setCacheHas; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a stack cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Stack(entries) { + var data = this.__data__ = new ListCache(entries); + this.size = data.size; + } + + /** + * Removes all key-value entries from the stack. + * + * @private + * @name clear + * @memberOf Stack + */ + function stackClear() { + this.__data__ = new ListCache; + this.size = 0; + } + + /** + * Removes `key` and its value from the stack. + * + * @private + * @name delete + * @memberOf Stack + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function stackDelete(key) { + var data = this.__data__, + result = data['delete'](key); + + this.size = data.size; + return result; + } + + /** + * Gets the stack value for `key`. + * + * @private + * @name get + * @memberOf Stack + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function stackGet(key) { + return this.__data__.get(key); + } + + /** + * Checks if a stack value for `key` exists. + * + * @private + * @name has + * @memberOf Stack + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function stackHas(key) { + return this.__data__.has(key); + } + + /** + * Sets the stack `key` to `value`. + * + * @private + * @name set + * @memberOf Stack + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the stack cache instance. + */ + function stackSet(key, value) { + var data = this.__data__; + if (data instanceof ListCache) { + var pairs = data.__data__; + if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + this.size = ++data.size; + return this; + } + data = this.__data__ = new MapCache(pairs); + } + data.set(key, value); + this.size = data.size; + return this; + } + + // Add methods to `Stack`. + Stack.prototype.clear = stackClear; + Stack.prototype['delete'] = stackDelete; + Stack.prototype.get = stackGet; + Stack.prototype.has = stackHas; + Stack.prototype.set = stackSet; + + /*------------------------------------------------------------------------*/ + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys(value, inherited) { + var isArr = isArray(value), + isArg = !isArr && isArguments(value), + isBuff = !isArr && !isArg && isBuffer(value), + isType = !isArr && !isArg && !isBuff && isTypedArray(value), + skipIndexes = isArr || isArg || isBuff || isType, + result = skipIndexes ? baseTimes(value.length, String) : [], + length = result.length; + + for (var key in value) { + if ((inherited || hasOwnProperty.call(value, key)) && + !(skipIndexes && ( + // Safari 9 has enumerable `arguments.length` in strict mode. + key == 'length' || + // Node.js 0.10 has enumerable non-index properties on buffers. + (isBuff && (key == 'offset' || key == 'parent')) || + // PhantomJS 2 has enumerable non-index properties on typed arrays. + (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) || + // Skip index properties. + isIndex(key, length) + ))) { + result.push(key); + } + } + return result; + } + + /** + * A specialized version of `_.sample` for arrays. + * + * @private + * @param {Array} array The array to sample. + * @returns {*} Returns the random element. + */ + function arraySample(array) { + var length = array.length; + return length ? array[baseRandom(0, length - 1)] : undefined; + } + + /** + * A specialized version of `_.sampleSize` for arrays. + * + * @private + * @param {Array} array The array to sample. + * @param {number} n The number of elements to sample. + * @returns {Array} Returns the random elements. + */ + function arraySampleSize(array, n) { + return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length)); + } + + /** + * A specialized version of `_.shuffle` for arrays. + * + * @private + * @param {Array} array The array to shuffle. + * @returns {Array} Returns the new shuffled array. + */ + function arrayShuffle(array) { + return shuffleSelf(copyArray(array)); + } + + /** + * This function is like `assignValue` except that it doesn't assign + * `undefined` values. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function assignMergeValue(object, key, value) { + if ((value !== undefined && !eq(object[key], value)) || + (value === undefined && !(key in object))) { + baseAssignValue(object, key, value); + } + } + + /** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + baseAssignValue(object, key, value); + } + } + + /** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; + } + + /** + * Aggregates elements of `collection` on `accumulator` with keys transformed + * by `iteratee` and values set by `setter`. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform keys. + * @param {Object} accumulator The initial aggregated object. + * @returns {Function} Returns `accumulator`. + */ + function baseAggregator(collection, setter, iteratee, accumulator) { + baseEach(collection, function(value, key, collection) { + setter(accumulator, value, iteratee(value), collection); + }); + return accumulator; + } + + /** + * The base implementation of `_.assign` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ + function baseAssign(object, source) { + return object && copyObject(source, keys(source), object); + } + + /** + * The base implementation of `_.assignIn` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ + function baseAssignIn(object, source) { + return object && copyObject(source, keysIn(source), object); + } + + /** + * The base implementation of `assignValue` and `assignMergeValue` without + * value checks. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function baseAssignValue(object, key, value) { + if (key == '__proto__' && defineProperty) { + defineProperty(object, key, { + 'configurable': true, + 'enumerable': true, + 'value': value, + 'writable': true + }); + } else { + object[key] = value; + } + } + + /** + * The base implementation of `_.at` without support for individual paths. + * + * @private + * @param {Object} object The object to iterate over. + * @param {string[]} paths The property paths to pick. + * @returns {Array} Returns the picked elements. + */ + function baseAt(object, paths) { + var index = -1, + length = paths.length, + result = Array(length), + skip = object == null; + + while (++index < length) { + result[index] = skip ? undefined : get(object, paths[index]); + } + return result; + } + + /** + * The base implementation of `_.clamp` which doesn't coerce arguments. + * + * @private + * @param {number} number The number to clamp. + * @param {number} [lower] The lower bound. + * @param {number} upper The upper bound. + * @returns {number} Returns the clamped number. + */ + function baseClamp(number, lower, upper) { + if (number === number) { + if (upper !== undefined) { + number = number <= upper ? number : upper; + } + if (lower !== undefined) { + number = number >= lower ? number : lower; + } + } + return number; + } + + /** + * The base implementation of `_.clone` and `_.cloneDeep` which tracks + * traversed objects. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} bitmask The bitmask flags. + * 1 - Deep clone + * 2 - Flatten inherited properties + * 4 - Clone symbols + * @param {Function} [customizer] The function to customize cloning. + * @param {string} [key] The key of `value`. + * @param {Object} [object] The parent object of `value`. + * @param {Object} [stack] Tracks traversed objects and their clone counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, bitmask, customizer, key, object, stack) { + var result, + isDeep = bitmask & CLONE_DEEP_FLAG, + isFlat = bitmask & CLONE_FLAT_FLAG, + isFull = bitmask & CLONE_SYMBOLS_FLAG; + + if (customizer) { + result = object ? customizer(value, key, object, stack) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return copyArray(value, result); + } + } else { + var tag = getTag(value), + isFunc = tag == funcTag || tag == genTag; + + if (isBuffer(value)) { + return cloneBuffer(value, isDeep); + } + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + result = (isFlat || isFunc) ? {} : initCloneObject(value); + if (!isDeep) { + return isFlat + ? copySymbolsIn(value, baseAssignIn(result, value)) + : copySymbols(value, baseAssign(result, value)); + } + } else { + if (!cloneableTags[tag]) { + return object ? value : {}; + } + result = initCloneByTag(value, tag, isDeep); + } + } + // Check for circular references and return its corresponding clone. + stack || (stack = new Stack); + var stacked = stack.get(value); + if (stacked) { + return stacked; + } + stack.set(value, result); + + if (isSet(value)) { + value.forEach(function(subValue) { + result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack)); + }); + + return result; + } + + if (isMap(value)) { + value.forEach(function(subValue, key) { + result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack)); + }); + + return result; + } + + var keysFunc = isFull + ? (isFlat ? getAllKeysIn : getAllKeys) + : (isFlat ? keysIn : keys); + + var props = isArr ? undefined : keysFunc(value); + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } + // Recursively populate clone (susceptible to call stack limits). + assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)); + }); + return result; + } + + /** + * The base implementation of `_.conforms` which doesn't clone `source`. + * + * @private + * @param {Object} source The object of property predicates to conform to. + * @returns {Function} Returns the new spec function. + */ + function baseConforms(source) { + var props = keys(source); + return function(object) { + return baseConformsTo(object, source, props); + }; + } + + /** + * The base implementation of `_.conformsTo` which accepts `props` to check. + * + * @private + * @param {Object} object The object to inspect. + * @param {Object} source The object of property predicates to conform to. + * @returns {boolean} Returns `true` if `object` conforms, else `false`. + */ + function baseConformsTo(object, source, props) { + var length = props.length; + if (object == null) { + return !length; + } + object = Object(object); + while (length--) { + var key = props[length], + predicate = source[key], + value = object[key]; + + if ((value === undefined && !(key in object)) || !predicate(value)) { + return false; + } + } + return true; + } + + /** + * The base implementation of `_.delay` and `_.defer` which accepts `args` + * to provide to `func`. + * + * @private + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @param {Array} args The arguments to provide to `func`. + * @returns {number|Object} Returns the timer id or timeout object. + */ + function baseDelay(func, wait, args) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + return setTimeout(function() { func.apply(undefined, args); }, wait); + } + + /** + * The base implementation of methods like `_.difference` without support + * for excluding multiple arrays or iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Array} values The values to exclude. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of filtered values. + */ + function baseDifference(array, values, iteratee, comparator) { + var index = -1, + includes = arrayIncludes, + isCommon = true, + length = array.length, + result = [], + valuesLength = values.length; + + if (!length) { + return result; + } + if (iteratee) { + values = arrayMap(values, baseUnary(iteratee)); + } + if (comparator) { + includes = arrayIncludesWith; + isCommon = false; + } + else if (values.length >= LARGE_ARRAY_SIZE) { + includes = cacheHas; + isCommon = false; + values = new SetCache(values); + } + outer: + while (++index < length) { + var value = array[index], + computed = iteratee == null ? value : iteratee(value); + + value = (comparator || value !== 0) ? value : 0; + if (isCommon && computed === computed) { + var valuesIndex = valuesLength; + while (valuesIndex--) { + if (values[valuesIndex] === computed) { + continue outer; + } + } + result.push(value); + } + else if (!includes(values, computed, comparator)) { + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.forEach` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + */ + var baseEach = createBaseEach(baseForOwn); + + /** + * The base implementation of `_.forEachRight` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + */ + var baseEachRight = createBaseEach(baseForOwnRight, true); + + /** + * The base implementation of `_.every` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false` + */ + function baseEvery(collection, predicate) { + var result = true; + baseEach(collection, function(value, index, collection) { + result = !!predicate(value, index, collection); + return result; + }); + return result; + } + + /** + * The base implementation of methods like `_.max` and `_.min` which accepts a + * `comparator` to determine the extremum value. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The iteratee invoked per iteration. + * @param {Function} comparator The comparator used to compare values. + * @returns {*} Returns the extremum value. + */ + function baseExtremum(array, iteratee, comparator) { + var index = -1, + length = array.length; + + while (++index < length) { + var value = array[index], + current = iteratee(value); + + if (current != null && (computed === undefined + ? (current === current && !isSymbol(current)) + : comparator(current, computed) + )) { + var computed = current, + result = value; + } + } + return result; + } + + /** + * The base implementation of `_.fill` without an iteratee call guard. + * + * @private + * @param {Array} array The array to fill. + * @param {*} value The value to fill `array` with. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns `array`. + */ + function baseFill(array, value, start, end) { + var length = array.length; + + start = toInteger(start); + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = (end === undefined || end > length) ? length : toInteger(end); + if (end < 0) { + end += length; + } + end = start > end ? 0 : toLength(end); + while (start < end) { + array[start++] = value; + } + return array; + } + + /** + * The base implementation of `_.filter` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ + function baseFilter(collection, predicate) { + var result = []; + baseEach(collection, function(value, index, collection) { + if (predicate(value, index, collection)) { + result.push(value); + } + }); + return result; + } + + /** + * The base implementation of `_.flatten` with support for restricting flattening. + * + * @private + * @param {Array} array The array to flatten. + * @param {number} depth The maximum recursion depth. + * @param {boolean} [predicate=isFlattenable] The function invoked per iteration. + * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. + * @param {Array} [result=[]] The initial result value. + * @returns {Array} Returns the new flattened array. + */ + function baseFlatten(array, depth, predicate, isStrict, result) { + var index = -1, + length = array.length; + + predicate || (predicate = isFlattenable); + result || (result = []); + + while (++index < length) { + var value = array[index]; + if (depth > 0 && predicate(value)) { + if (depth > 1) { + // Recursively flatten arrays (susceptible to call stack limits). + baseFlatten(value, depth - 1, predicate, isStrict, result); + } else { + arrayPush(result, value); + } + } else if (!isStrict) { + result[result.length] = value; + } + } + return result; + } + + /** + * The base implementation of `baseForOwn` which iterates over `object` + * properties returned by `keysFunc` and invokes `iteratee` for each property. + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseFor = createBaseFor(); + + /** + * This function is like `baseFor` except that it iterates over properties + * in the opposite order. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseForRight = createBaseFor(true); + + /** + * The base implementation of `_.forOwn` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwn(object, iteratee) { + return object && baseFor(object, iteratee, keys); + } + + /** + * The base implementation of `_.forOwnRight` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwnRight(object, iteratee) { + return object && baseForRight(object, iteratee, keys); + } + + /** + * The base implementation of `_.functions` which creates an array of + * `object` function property names filtered from `props`. + * + * @private + * @param {Object} object The object to inspect. + * @param {Array} props The property names to filter. + * @returns {Array} Returns the function names. + */ + function baseFunctions(object, props) { + return arrayFilter(props, function(key) { + return isFunction(object[key]); + }); + } + + /** + * The base implementation of `_.get` without support for default values. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @returns {*} Returns the resolved value. + */ + function baseGet(object, path) { + path = castPath(path, object); + + var index = 0, + length = path.length; + + while (object != null && index < length) { + object = object[toKey(path[index++])]; + } + return (index && index == length) ? object : undefined; + } + + /** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ + function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); + } + + /** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag && symToStringTag in Object(value)) + ? getRawTag(value) + : objectToString(value); + } + + /** + * The base implementation of `_.gt` which doesn't coerce arguments. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than `other`, + * else `false`. + */ + function baseGt(value, other) { + return value > other; + } + + /** + * The base implementation of `_.has` without support for deep paths. + * + * @private + * @param {Object} [object] The object to query. + * @param {Array|string} key The key to check. + * @returns {boolean} Returns `true` if `key` exists, else `false`. + */ + function baseHas(object, key) { + return object != null && hasOwnProperty.call(object, key); + } + + /** + * The base implementation of `_.hasIn` without support for deep paths. + * + * @private + * @param {Object} [object] The object to query. + * @param {Array|string} key The key to check. + * @returns {boolean} Returns `true` if `key` exists, else `false`. + */ + function baseHasIn(object, key) { + return object != null && key in Object(object); + } + + /** + * The base implementation of `_.inRange` which doesn't coerce arguments. + * + * @private + * @param {number} number The number to check. + * @param {number} start The start of the range. + * @param {number} end The end of the range. + * @returns {boolean} Returns `true` if `number` is in the range, else `false`. + */ + function baseInRange(number, start, end) { + return number >= nativeMin(start, end) && number < nativeMax(start, end); + } + + /** + * The base implementation of methods like `_.intersection`, without support + * for iteratee shorthands, that accepts an array of arrays to inspect. + * + * @private + * @param {Array} arrays The arrays to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of shared values. + */ + function baseIntersection(arrays, iteratee, comparator) { + var includes = comparator ? arrayIncludesWith : arrayIncludes, + length = arrays[0].length, + othLength = arrays.length, + othIndex = othLength, + caches = Array(othLength), + maxLength = Infinity, + result = []; + + while (othIndex--) { + var array = arrays[othIndex]; + if (othIndex && iteratee) { + array = arrayMap(array, baseUnary(iteratee)); + } + maxLength = nativeMin(array.length, maxLength); + caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120)) + ? new SetCache(othIndex && array) + : undefined; + } + array = arrays[0]; + + var index = -1, + seen = caches[0]; + + outer: + while (++index < length && result.length < maxLength) { + var value = array[index], + computed = iteratee ? iteratee(value) : value; + + value = (comparator || value !== 0) ? value : 0; + if (!(seen + ? cacheHas(seen, computed) + : includes(result, computed, comparator) + )) { + othIndex = othLength; + while (--othIndex) { + var cache = caches[othIndex]; + if (!(cache + ? cacheHas(cache, computed) + : includes(arrays[othIndex], computed, comparator)) + ) { + continue outer; + } + } + if (seen) { + seen.push(computed); + } + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.invert` and `_.invertBy` which inverts + * `object` with values transformed by `iteratee` and set by `setter`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform values. + * @param {Object} accumulator The initial inverted object. + * @returns {Function} Returns `accumulator`. + */ + function baseInverter(object, setter, iteratee, accumulator) { + baseForOwn(object, function(value, key, object) { + setter(accumulator, iteratee(value), key, object); + }); + return accumulator; + } + + /** + * The base implementation of `_.invoke` without support for individual + * method arguments. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path of the method to invoke. + * @param {Array} args The arguments to invoke the method with. + * @returns {*} Returns the result of the invoked method. + */ + function baseInvoke(object, path, args) { + path = castPath(path, object); + object = parent(object, path); + var func = object == null ? object : object[toKey(last(path))]; + return func == null ? undefined : apply(func, object, args); + } + + /** + * The base implementation of `_.isArguments`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + */ + function baseIsArguments(value) { + return isObjectLike(value) && baseGetTag(value) == argsTag; + } + + /** + * The base implementation of `_.isArrayBuffer` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. + */ + function baseIsArrayBuffer(value) { + return isObjectLike(value) && baseGetTag(value) == arrayBufferTag; + } + + /** + * The base implementation of `_.isDate` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a date object, else `false`. + */ + function baseIsDate(value) { + return isObjectLike(value) && baseGetTag(value) == dateTag; + } + + /** + * The base implementation of `_.isEqual` which supports partial comparisons + * and tracks traversed objects. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {boolean} bitmask The bitmask flags. + * 1 - Unordered comparison + * 2 - Partial comparison + * @param {Function} [customizer] The function to customize comparisons. + * @param {Object} [stack] Tracks traversed `value` and `other` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ + function baseIsEqual(value, other, bitmask, customizer, stack) { + if (value === other) { + return true; + } + if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) { + return value !== value && other !== other; + } + return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack); + } + + /** + * A specialized version of `baseIsEqual` for arrays and objects which performs + * deep comparisons and tracks traversed objects enabling objects with circular + * references to be compared. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} [stack] Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) { + var objIsArr = isArray(object), + othIsArr = isArray(other), + objTag = objIsArr ? arrayTag : getTag(object), + othTag = othIsArr ? arrayTag : getTag(other); + + objTag = objTag == argsTag ? objectTag : objTag; + othTag = othTag == argsTag ? objectTag : othTag; + + var objIsObj = objTag == objectTag, + othIsObj = othTag == objectTag, + isSameTag = objTag == othTag; + + if (isSameTag && isBuffer(object)) { + if (!isBuffer(other)) { + return false; + } + objIsArr = true; + objIsObj = false; + } + if (isSameTag && !objIsObj) { + stack || (stack = new Stack); + return (objIsArr || isTypedArray(object)) + ? equalArrays(object, other, bitmask, customizer, equalFunc, stack) + : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack); + } + if (!(bitmask & COMPARE_PARTIAL_FLAG)) { + var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), + othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); + + if (objIsWrapped || othIsWrapped) { + var objUnwrapped = objIsWrapped ? object.value() : object, + othUnwrapped = othIsWrapped ? other.value() : other; + + stack || (stack = new Stack); + return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack); + } + } + if (!isSameTag) { + return false; + } + stack || (stack = new Stack); + return equalObjects(object, other, bitmask, customizer, equalFunc, stack); + } + + /** + * The base implementation of `_.isMap` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a map, else `false`. + */ + function baseIsMap(value) { + return isObjectLike(value) && getTag(value) == mapTag; + } + + /** + * The base implementation of `_.isMatch` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @param {Array} matchData The property names, values, and compare flags to match. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + */ + function baseIsMatch(object, source, matchData, customizer) { + var index = matchData.length, + length = index, + noCustomizer = !customizer; + + if (object == null) { + return !length; + } + object = Object(object); + while (index--) { + var data = matchData[index]; + if ((noCustomizer && data[2]) + ? data[1] !== object[data[0]] + : !(data[0] in object) + ) { + return false; + } + } + while (++index < length) { + data = matchData[index]; + var key = data[0], + objValue = object[key], + srcValue = data[1]; + + if (noCustomizer && data[2]) { + if (objValue === undefined && !(key in object)) { + return false; + } + } else { + var stack = new Stack; + if (customizer) { + var result = customizer(objValue, srcValue, key, object, source, stack); + } + if (!(result === undefined + ? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG, customizer, stack) + : result + )) { + return false; + } + } + } + return true; + } + + /** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ + function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = isFunction(value) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); + } + + /** + * The base implementation of `_.isRegExp` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. + */ + function baseIsRegExp(value) { + return isObjectLike(value) && baseGetTag(value) == regexpTag; + } + + /** + * The base implementation of `_.isSet` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a set, else `false`. + */ + function baseIsSet(value) { + return isObjectLike(value) && getTag(value) == setTag; + } + + /** + * The base implementation of `_.isTypedArray` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. + */ + function baseIsTypedArray(value) { + return isObjectLike(value) && + isLength(value.length) && !!typedArrayTags[baseGetTag(value)]; + } + + /** + * The base implementation of `_.iteratee`. + * + * @private + * @param {*} [value=_.identity] The value to convert to an iteratee. + * @returns {Function} Returns the iteratee. + */ + function baseIteratee(value) { + // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. + // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. + if (typeof value == 'function') { + return value; + } + if (value == null) { + return identity; + } + if (typeof value == 'object') { + return isArray(value) + ? baseMatchesProperty(value[0], value[1]) + : baseMatches(value); + } + return property(value); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeysIn(object) { + if (!isObject(object)) { + return nativeKeysIn(object); + } + var isProto = isPrototype(object), + result = []; + + for (var key in object) { + if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `_.lt` which doesn't coerce arguments. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than `other`, + * else `false`. + */ + function baseLt(value, other) { + return value < other; + } + + /** + * The base implementation of `_.map` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function baseMap(collection, iteratee) { + var index = -1, + result = isArrayLike(collection) ? Array(collection.length) : []; + + baseEach(collection, function(value, key, collection) { + result[++index] = iteratee(value, key, collection); + }); + return result; + } + + /** + * The base implementation of `_.matches` which doesn't clone `source`. + * + * @private + * @param {Object} source The object of property values to match. + * @returns {Function} Returns the new spec function. + */ + function baseMatches(source) { + var matchData = getMatchData(source); + if (matchData.length == 1 && matchData[0][2]) { + return matchesStrictComparable(matchData[0][0], matchData[0][1]); + } + return function(object) { + return object === source || baseIsMatch(object, source, matchData); + }; + } + + /** + * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`. + * + * @private + * @param {string} path The path of the property to get. + * @param {*} srcValue The value to match. + * @returns {Function} Returns the new spec function. + */ + function baseMatchesProperty(path, srcValue) { + if (isKey(path) && isStrictComparable(srcValue)) { + return matchesStrictComparable(toKey(path), srcValue); + } + return function(object) { + var objValue = get(object, path); + return (objValue === undefined && objValue === srcValue) + ? hasIn(object, path) + : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG); + }; + } + + /** + * The base implementation of `_.merge` without support for multiple sources. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {number} srcIndex The index of `source`. + * @param {Function} [customizer] The function to customize merged values. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + */ + function baseMerge(object, source, srcIndex, customizer, stack) { + if (object === source) { + return; + } + baseFor(source, function(srcValue, key) { + if (isObject(srcValue)) { + stack || (stack = new Stack); + baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack); + } + else { + var newValue = customizer + ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack) + : undefined; + + if (newValue === undefined) { + newValue = srcValue; + } + assignMergeValue(object, key, newValue); + } + }, keysIn); + } + + /** + * A specialized version of `baseMerge` for arrays and objects which performs + * deep merges and tracks traversed objects enabling objects with circular + * references to be merged. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {string} key The key of the value to merge. + * @param {number} srcIndex The index of `source`. + * @param {Function} mergeFunc The function to merge values. + * @param {Function} [customizer] The function to customize assigned values. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + */ + function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) { + var objValue = safeGet(object, key), + srcValue = safeGet(source, key), + stacked = stack.get(srcValue); + + if (stacked) { + assignMergeValue(object, key, stacked); + return; + } + var newValue = customizer + ? customizer(objValue, srcValue, (key + ''), object, source, stack) + : undefined; + + var isCommon = newValue === undefined; + + if (isCommon) { + var isArr = isArray(srcValue), + isBuff = !isArr && isBuffer(srcValue), + isTyped = !isArr && !isBuff && isTypedArray(srcValue); + + newValue = srcValue; + if (isArr || isBuff || isTyped) { + if (isArray(objValue)) { + newValue = objValue; + } + else if (isArrayLikeObject(objValue)) { + newValue = copyArray(objValue); + } + else if (isBuff) { + isCommon = false; + newValue = cloneBuffer(srcValue, true); + } + else if (isTyped) { + isCommon = false; + newValue = cloneTypedArray(srcValue, true); + } + else { + newValue = []; + } + } + else if (isPlainObject(srcValue) || isArguments(srcValue)) { + newValue = objValue; + if (isArguments(objValue)) { + newValue = toPlainObject(objValue); + } + else if (!isObject(objValue) || (srcIndex && isFunction(objValue))) { + newValue = initCloneObject(srcValue); + } + } + else { + isCommon = false; + } + } + if (isCommon) { + // Recursively merge objects and arrays (susceptible to call stack limits). + stack.set(srcValue, newValue); + mergeFunc(newValue, srcValue, srcIndex, customizer, stack); + stack['delete'](srcValue); + } + assignMergeValue(object, key, newValue); + } + + /** + * The base implementation of `_.nth` which doesn't coerce arguments. + * + * @private + * @param {Array} array The array to query. + * @param {number} n The index of the element to return. + * @returns {*} Returns the nth element of `array`. + */ + function baseNth(array, n) { + var length = array.length; + if (!length) { + return; + } + n += n < 0 ? length : 0; + return isIndex(n, length) ? array[n] : undefined; + } + + /** + * The base implementation of `_.orderBy` without param guards. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by. + * @param {string[]} orders The sort orders of `iteratees`. + * @returns {Array} Returns the new sorted array. + */ + function baseOrderBy(collection, iteratees, orders) { + var index = -1; + iteratees = arrayMap(iteratees.length ? iteratees : [identity], baseUnary(getIteratee())); + + var result = baseMap(collection, function(value, key, collection) { + var criteria = arrayMap(iteratees, function(iteratee) { + return iteratee(value); + }); + return { 'criteria': criteria, 'index': ++index, 'value': value }; + }); + + return baseSortBy(result, function(object, other) { + return compareMultiple(object, other, orders); + }); + } + + /** + * The base implementation of `_.pick` without support for individual + * property identifiers. + * + * @private + * @param {Object} object The source object. + * @param {string[]} paths The property paths to pick. + * @returns {Object} Returns the new object. + */ + function basePick(object, paths) { + return basePickBy(object, paths, function(value, path) { + return hasIn(object, path); + }); + } + + /** + * The base implementation of `_.pickBy` without support for iteratee shorthands. + * + * @private + * @param {Object} object The source object. + * @param {string[]} paths The property paths to pick. + * @param {Function} predicate The function invoked per property. + * @returns {Object} Returns the new object. + */ + function basePickBy(object, paths, predicate) { + var index = -1, + length = paths.length, + result = {}; + + while (++index < length) { + var path = paths[index], + value = baseGet(object, path); + + if (predicate(value, path)) { + baseSet(result, castPath(path, object), value); + } + } + return result; + } + + /** + * A specialized version of `baseProperty` which supports deep paths. + * + * @private + * @param {Array|string} path The path of the property to get. + * @returns {Function} Returns the new accessor function. + */ + function basePropertyDeep(path) { + return function(object) { + return baseGet(object, path); + }; + } + + /** + * The base implementation of `_.pullAllBy` without support for iteratee + * shorthands. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns `array`. + */ + function basePullAll(array, values, iteratee, comparator) { + var indexOf = comparator ? baseIndexOfWith : baseIndexOf, + index = -1, + length = values.length, + seen = array; + + if (array === values) { + values = copyArray(values); + } + if (iteratee) { + seen = arrayMap(array, baseUnary(iteratee)); + } + while (++index < length) { + var fromIndex = 0, + value = values[index], + computed = iteratee ? iteratee(value) : value; + + while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) { + if (seen !== array) { + splice.call(seen, fromIndex, 1); + } + splice.call(array, fromIndex, 1); + } + } + return array; + } + + /** + * The base implementation of `_.pullAt` without support for individual + * indexes or capturing the removed elements. + * + * @private + * @param {Array} array The array to modify. + * @param {number[]} indexes The indexes of elements to remove. + * @returns {Array} Returns `array`. + */ + function basePullAt(array, indexes) { + var length = array ? indexes.length : 0, + lastIndex = length - 1; + + while (length--) { + var index = indexes[length]; + if (length == lastIndex || index !== previous) { + var previous = index; + if (isIndex(index)) { + splice.call(array, index, 1); + } else { + baseUnset(array, index); + } + } + } + return array; + } + + /** + * The base implementation of `_.random` without support for returning + * floating-point numbers. + * + * @private + * @param {number} lower The lower bound. + * @param {number} upper The upper bound. + * @returns {number} Returns the random number. + */ + function baseRandom(lower, upper) { + return lower + nativeFloor(nativeRandom() * (upper - lower + 1)); + } + + /** + * The base implementation of `_.range` and `_.rangeRight` which doesn't + * coerce arguments. + * + * @private + * @param {number} start The start of the range. + * @param {number} end The end of the range. + * @param {number} step The value to increment or decrement by. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Array} Returns the range of numbers. + */ + function baseRange(start, end, step, fromRight) { + var index = -1, + length = nativeMax(nativeCeil((end - start) / (step || 1)), 0), + result = Array(length); + + while (length--) { + result[fromRight ? length : ++index] = start; + start += step; + } + return result; + } + + /** + * The base implementation of `_.repeat` which doesn't coerce arguments. + * + * @private + * @param {string} string The string to repeat. + * @param {number} n The number of times to repeat the string. + * @returns {string} Returns the repeated string. + */ + function baseRepeat(string, n) { + var result = ''; + if (!string || n < 1 || n > MAX_SAFE_INTEGER) { + return result; + } + // Leverage the exponentiation by squaring algorithm for a faster repeat. + // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details. + do { + if (n % 2) { + result += string; + } + n = nativeFloor(n / 2); + if (n) { + string += string; + } + } while (n); + + return result; + } + + /** + * The base implementation of `_.rest` which doesn't validate or coerce arguments. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + */ + function baseRest(func, start) { + return setToString(overRest(func, start, identity), func + ''); + } + + /** + * The base implementation of `_.sample`. + * + * @private + * @param {Array|Object} collection The collection to sample. + * @returns {*} Returns the random element. + */ + function baseSample(collection) { + return arraySample(values(collection)); + } + + /** + * The base implementation of `_.sampleSize` without param guards. + * + * @private + * @param {Array|Object} collection The collection to sample. + * @param {number} n The number of elements to sample. + * @returns {Array} Returns the random elements. + */ + function baseSampleSize(collection, n) { + var array = values(collection); + return shuffleSelf(array, baseClamp(n, 0, array.length)); + } + + /** + * The base implementation of `_.set`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize path creation. + * @returns {Object} Returns `object`. + */ + function baseSet(object, path, value, customizer) { + if (!isObject(object)) { + return object; + } + path = castPath(path, object); + + var index = -1, + length = path.length, + lastIndex = length - 1, + nested = object; + + while (nested != null && ++index < length) { + var key = toKey(path[index]), + newValue = value; + + if (index != lastIndex) { + var objValue = nested[key]; + newValue = customizer ? customizer(objValue, key, nested) : undefined; + if (newValue === undefined) { + newValue = isObject(objValue) + ? objValue + : (isIndex(path[index + 1]) ? [] : {}); + } + } + assignValue(nested, key, newValue); + nested = nested[key]; + } + return object; + } + + /** + * The base implementation of `setData` without support for hot loop shorting. + * + * @private + * @param {Function} func The function to associate metadata with. + * @param {*} data The metadata. + * @returns {Function} Returns `func`. + */ + var baseSetData = !metaMap ? identity : function(func, data) { + metaMap.set(func, data); + return func; + }; + + /** + * The base implementation of `setToString` without support for hot loop shorting. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ + var baseSetToString = !defineProperty ? identity : function(func, string) { + return defineProperty(func, 'toString', { + 'configurable': true, + 'enumerable': false, + 'value': constant(string), + 'writable': true + }); + }; + + /** + * The base implementation of `_.shuffle`. + * + * @private + * @param {Array|Object} collection The collection to shuffle. + * @returns {Array} Returns the new shuffled array. + */ + function baseShuffle(collection) { + return shuffleSelf(values(collection)); + } + + /** + * The base implementation of `_.slice` without an iteratee call guard. + * + * @private + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function baseSlice(array, start, end) { + var index = -1, + length = array.length; + + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = end > length ? length : end; + if (end < 0) { + end += length; + } + length = start > end ? 0 : ((end - start) >>> 0); + start >>>= 0; + + var result = Array(length); + while (++index < length) { + result[index] = array[index + start]; + } + return result; + } + + /** + * The base implementation of `_.some` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + */ + function baseSome(collection, predicate) { + var result; + + baseEach(collection, function(value, index, collection) { + result = predicate(value, index, collection); + return !result; + }); + return !!result; + } + + /** + * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which + * performs a binary search of `array` to determine the index at which `value` + * should be inserted into `array` in order to maintain its sort order. + * + * @private + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + */ + function baseSortedIndex(array, value, retHighest) { + var low = 0, + high = array == null ? low : array.length; + + if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { + while (low < high) { + var mid = (low + high) >>> 1, + computed = array[mid]; + + if (computed !== null && !isSymbol(computed) && + (retHighest ? (computed <= value) : (computed < value))) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + return baseSortedIndexBy(array, value, identity, retHighest); + } + + /** + * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy` + * which invokes `iteratee` for `value` and each element of `array` to compute + * their sort ranking. The iteratee is invoked with one argument; (value). + * + * @private + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} iteratee The iteratee invoked per element. + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + */ + function baseSortedIndexBy(array, value, iteratee, retHighest) { + value = iteratee(value); + + var low = 0, + high = array == null ? 0 : array.length, + valIsNaN = value !== value, + valIsNull = value === null, + valIsSymbol = isSymbol(value), + valIsUndefined = value === undefined; + + while (low < high) { + var mid = nativeFloor((low + high) / 2), + computed = iteratee(array[mid]), + othIsDefined = computed !== undefined, + othIsNull = computed === null, + othIsReflexive = computed === computed, + othIsSymbol = isSymbol(computed); + + if (valIsNaN) { + var setLow = retHighest || othIsReflexive; + } else if (valIsUndefined) { + setLow = othIsReflexive && (retHighest || othIsDefined); + } else if (valIsNull) { + setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull); + } else if (valIsSymbol) { + setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol); + } else if (othIsNull || othIsSymbol) { + setLow = false; + } else { + setLow = retHighest ? (computed <= value) : (computed < value); + } + if (setLow) { + low = mid + 1; + } else { + high = mid; + } + } + return nativeMin(high, MAX_ARRAY_INDEX); + } + + /** + * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without + * support for iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @returns {Array} Returns the new duplicate free array. + */ + function baseSortedUniq(array, iteratee) { + var index = -1, + length = array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index], + computed = iteratee ? iteratee(value) : value; + + if (!index || !eq(computed, seen)) { + var seen = computed; + result[resIndex++] = value === 0 ? 0 : value; + } + } + return result; + } + + /** + * The base implementation of `_.toNumber` which doesn't ensure correct + * conversions of binary, hexadecimal, or octal string values. + * + * @private + * @param {*} value The value to process. + * @returns {number} Returns the number. + */ + function baseToNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + return +value; + } + + /** + * The base implementation of `_.toString` which doesn't convert nullish + * values to empty strings. + * + * @private + * @param {*} value The value to process. + * @returns {string} Returns the string. + */ + function baseToString(value) { + // Exit early for strings to avoid a performance hit in some environments. + if (typeof value == 'string') { + return value; + } + if (isArray(value)) { + // Recursively convert values (susceptible to call stack limits). + return arrayMap(value, baseToString) + ''; + } + if (isSymbol(value)) { + return symbolToString ? symbolToString.call(value) : ''; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; + } + + /** + * The base implementation of `_.uniqBy` without support for iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new duplicate free array. + */ + function baseUniq(array, iteratee, comparator) { + var index = -1, + includes = arrayIncludes, + length = array.length, + isCommon = true, + result = [], + seen = result; + + if (comparator) { + isCommon = false; + includes = arrayIncludesWith; + } + else if (length >= LARGE_ARRAY_SIZE) { + var set = iteratee ? null : createSet(array); + if (set) { + return setToArray(set); + } + isCommon = false; + includes = cacheHas; + seen = new SetCache; + } + else { + seen = iteratee ? [] : result; + } + outer: + while (++index < length) { + var value = array[index], + computed = iteratee ? iteratee(value) : value; + + value = (comparator || value !== 0) ? value : 0; + if (isCommon && computed === computed) { + var seenIndex = seen.length; + while (seenIndex--) { + if (seen[seenIndex] === computed) { + continue outer; + } + } + if (iteratee) { + seen.push(computed); + } + result.push(value); + } + else if (!includes(seen, computed, comparator)) { + if (seen !== result) { + seen.push(computed); + } + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.unset`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The property path to unset. + * @returns {boolean} Returns `true` if the property is deleted, else `false`. + */ + function baseUnset(object, path) { + path = castPath(path, object); + object = parent(object, path); + return object == null || delete object[toKey(last(path))]; + } + + /** + * The base implementation of `_.update`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to update. + * @param {Function} updater The function to produce the updated value. + * @param {Function} [customizer] The function to customize path creation. + * @returns {Object} Returns `object`. + */ + function baseUpdate(object, path, updater, customizer) { + return baseSet(object, path, updater(baseGet(object, path)), customizer); + } + + /** + * The base implementation of methods like `_.dropWhile` and `_.takeWhile` + * without support for iteratee shorthands. + * + * @private + * @param {Array} array The array to query. + * @param {Function} predicate The function invoked per iteration. + * @param {boolean} [isDrop] Specify dropping elements instead of taking them. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Array} Returns the slice of `array`. + */ + function baseWhile(array, predicate, isDrop, fromRight) { + var length = array.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length) && + predicate(array[index], index, array)) {} + + return isDrop + ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) + : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); + } + + /** + * The base implementation of `wrapperValue` which returns the result of + * performing a sequence of actions on the unwrapped `value`, where each + * successive action is supplied the return value of the previous. + * + * @private + * @param {*} value The unwrapped value. + * @param {Array} actions Actions to perform to resolve the unwrapped value. + * @returns {*} Returns the resolved value. + */ + function baseWrapperValue(value, actions) { + var result = value; + if (result instanceof LazyWrapper) { + result = result.value(); + } + return arrayReduce(actions, function(result, action) { + return action.func.apply(action.thisArg, arrayPush([result], action.args)); + }, result); + } + + /** + * The base implementation of methods like `_.xor`, without support for + * iteratee shorthands, that accepts an array of arrays to inspect. + * + * @private + * @param {Array} arrays The arrays to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of values. + */ + function baseXor(arrays, iteratee, comparator) { + var length = arrays.length; + if (length < 2) { + return length ? baseUniq(arrays[0]) : []; + } + var index = -1, + result = Array(length); + + while (++index < length) { + var array = arrays[index], + othIndex = -1; + + while (++othIndex < length) { + if (othIndex != index) { + result[index] = baseDifference(result[index] || array, arrays[othIndex], iteratee, comparator); + } + } + } + return baseUniq(baseFlatten(result, 1), iteratee, comparator); + } + + /** + * This base implementation of `_.zipObject` which assigns values using `assignFunc`. + * + * @private + * @param {Array} props The property identifiers. + * @param {Array} values The property values. + * @param {Function} assignFunc The function to assign values. + * @returns {Object} Returns the new object. + */ + function baseZipObject(props, values, assignFunc) { + var index = -1, + length = props.length, + valsLength = values.length, + result = {}; + + while (++index < length) { + var value = index < valsLength ? values[index] : undefined; + assignFunc(result, props[index], value); + } + return result; + } + + /** + * Casts `value` to an empty array if it's not an array like object. + * + * @private + * @param {*} value The value to inspect. + * @returns {Array|Object} Returns the cast array-like object. + */ + function castArrayLikeObject(value) { + return isArrayLikeObject(value) ? value : []; + } + + /** + * Casts `value` to `identity` if it's not a function. + * + * @private + * @param {*} value The value to inspect. + * @returns {Function} Returns cast function. + */ + function castFunction(value) { + return typeof value == 'function' ? value : identity; + } + + /** + * Casts `value` to a path array if it's not one. + * + * @private + * @param {*} value The value to inspect. + * @param {Object} [object] The object to query keys on. + * @returns {Array} Returns the cast property path array. + */ + function castPath(value, object) { + if (isArray(value)) { + return value; + } + return isKey(value, object) ? [value] : stringToPath(toString(value)); + } + + /** + * A `baseRest` alias which can be replaced with `identity` by module + * replacement plugins. + * + * @private + * @type {Function} + * @param {Function} func The function to apply a rest parameter to. + * @returns {Function} Returns the new function. + */ + var castRest = baseRest; + + /** + * Casts `array` to a slice if it's needed. + * + * @private + * @param {Array} array The array to inspect. + * @param {number} start The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the cast slice. + */ + function castSlice(array, start, end) { + var length = array.length; + end = end === undefined ? length : end; + return (!start && end >= length) ? array : baseSlice(array, start, end); + } + + /** + * A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout). + * + * @private + * @param {number|Object} id The timer id or timeout object of the timer to clear. + */ + var clearTimeout = ctxClearTimeout || function(id) { + return root.clearTimeout(id); + }; + + /** + * Creates a clone of `buffer`. + * + * @private + * @param {Buffer} buffer The buffer to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Buffer} Returns the cloned buffer. + */ + function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var length = buffer.length, + result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length); + + buffer.copy(result); + return result; + } + + /** + * Creates a clone of `arrayBuffer`. + * + * @private + * @param {ArrayBuffer} arrayBuffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ + function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; + } + + /** + * Creates a clone of `dataView`. + * + * @private + * @param {Object} dataView The data view to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned data view. + */ + function cloneDataView(dataView, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; + return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); + } + + /** + * Creates a clone of `regexp`. + * + * @private + * @param {Object} regexp The regexp to clone. + * @returns {Object} Returns the cloned regexp. + */ + function cloneRegExp(regexp) { + var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); + result.lastIndex = regexp.lastIndex; + return result; + } + + /** + * Creates a clone of the `symbol` object. + * + * @private + * @param {Object} symbol The symbol object to clone. + * @returns {Object} Returns the cloned symbol object. + */ + function cloneSymbol(symbol) { + return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; + } + + /** + * Creates a clone of `typedArray`. + * + * @private + * @param {Object} typedArray The typed array to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned typed array. + */ + function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); + } + + /** + * Compares values to sort them in ascending order. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {number} Returns the sort order indicator for `value`. + */ + function compareAscending(value, other) { + if (value !== other) { + var valIsDefined = value !== undefined, + valIsNull = value === null, + valIsReflexive = value === value, + valIsSymbol = isSymbol(value); + + var othIsDefined = other !== undefined, + othIsNull = other === null, + othIsReflexive = other === other, + othIsSymbol = isSymbol(other); + + if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) || + (valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) || + (valIsNull && othIsDefined && othIsReflexive) || + (!valIsDefined && othIsReflexive) || + !valIsReflexive) { + return 1; + } + if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) || + (othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) || + (othIsNull && valIsDefined && valIsReflexive) || + (!othIsDefined && valIsReflexive) || + !othIsReflexive) { + return -1; + } + } + return 0; + } + + /** + * Used by `_.orderBy` to compare multiple properties of a value to another + * and stable sort them. + * + * If `orders` is unspecified, all values are sorted in ascending order. Otherwise, + * specify an order of "desc" for descending or "asc" for ascending sort order + * of corresponding values. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {boolean[]|string[]} orders The order to sort by for each property. + * @returns {number} Returns the sort order indicator for `object`. + */ + function compareMultiple(object, other, orders) { + var index = -1, + objCriteria = object.criteria, + othCriteria = other.criteria, + length = objCriteria.length, + ordersLength = orders.length; + + while (++index < length) { + var result = compareAscending(objCriteria[index], othCriteria[index]); + if (result) { + if (index >= ordersLength) { + return result; + } + var order = orders[index]; + return result * (order == 'desc' ? -1 : 1); + } + } + // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications + // that causes it, under certain circumstances, to provide the same value for + // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247 + // for more details. + // + // This also ensures a stable sort in V8 and other engines. + // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details. + return object.index - other.index; + } + + /** + * Creates an array that is the composition of partially applied arguments, + * placeholders, and provided arguments into a single array of arguments. + * + * @private + * @param {Array} args The provided arguments. + * @param {Array} partials The arguments to prepend to those provided. + * @param {Array} holders The `partials` placeholder indexes. + * @params {boolean} [isCurried] Specify composing for a curried function. + * @returns {Array} Returns the new array of composed arguments. + */ + function composeArgs(args, partials, holders, isCurried) { + var argsIndex = -1, + argsLength = args.length, + holdersLength = holders.length, + leftIndex = -1, + leftLength = partials.length, + rangeLength = nativeMax(argsLength - holdersLength, 0), + result = Array(leftLength + rangeLength), + isUncurried = !isCurried; + + while (++leftIndex < leftLength) { + result[leftIndex] = partials[leftIndex]; + } + while (++argsIndex < holdersLength) { + if (isUncurried || argsIndex < argsLength) { + result[holders[argsIndex]] = args[argsIndex]; + } + } + while (rangeLength--) { + result[leftIndex++] = args[argsIndex++]; + } + return result; + } + + /** + * This function is like `composeArgs` except that the arguments composition + * is tailored for `_.partialRight`. + * + * @private + * @param {Array} args The provided arguments. + * @param {Array} partials The arguments to append to those provided. + * @param {Array} holders The `partials` placeholder indexes. + * @params {boolean} [isCurried] Specify composing for a curried function. + * @returns {Array} Returns the new array of composed arguments. + */ + function composeArgsRight(args, partials, holders, isCurried) { + var argsIndex = -1, + argsLength = args.length, + holdersIndex = -1, + holdersLength = holders.length, + rightIndex = -1, + rightLength = partials.length, + rangeLength = nativeMax(argsLength - holdersLength, 0), + result = Array(rangeLength + rightLength), + isUncurried = !isCurried; + + while (++argsIndex < rangeLength) { + result[argsIndex] = args[argsIndex]; + } + var offset = argsIndex; + while (++rightIndex < rightLength) { + result[offset + rightIndex] = partials[rightIndex]; + } + while (++holdersIndex < holdersLength) { + if (isUncurried || argsIndex < argsLength) { + result[offset + holders[holdersIndex]] = args[argsIndex++]; + } + } + return result; + } + + /** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ + function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; + } + + /** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property identifiers to copy. + * @param {Object} [object={}] The object to copy properties to. + * @param {Function} [customizer] The function to customize copied values. + * @returns {Object} Returns `object`. + */ + function copyObject(source, props, object, customizer) { + var isNew = !object; + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + if (newValue === undefined) { + newValue = source[key]; + } + if (isNew) { + baseAssignValue(object, key, newValue); + } else { + assignValue(object, key, newValue); + } + } + return object; + } + + /** + * Copies own symbols of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ + function copySymbols(source, object) { + return copyObject(source, getSymbols(source), object); + } + + /** + * Copies own and inherited symbols of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ + function copySymbolsIn(source, object) { + return copyObject(source, getSymbolsIn(source), object); + } + + /** + * Creates a function like `_.groupBy`. + * + * @private + * @param {Function} setter The function to set accumulator values. + * @param {Function} [initializer] The accumulator object initializer. + * @returns {Function} Returns the new aggregator function. + */ + function createAggregator(setter, initializer) { + return function(collection, iteratee) { + var func = isArray(collection) ? arrayAggregator : baseAggregator, + accumulator = initializer ? initializer() : {}; + + return func(collection, setter, getIteratee(iteratee, 2), accumulator); + }; + } + + /** + * Creates a function like `_.assign`. + * + * @private + * @param {Function} assigner The function to assign values. + * @returns {Function} Returns the new assigner function. + */ + function createAssigner(assigner) { + return baseRest(function(object, sources) { + var index = -1, + length = sources.length, + customizer = length > 1 ? sources[length - 1] : undefined, + guard = length > 2 ? sources[2] : undefined; + + customizer = (assigner.length > 3 && typeof customizer == 'function') + ? (length--, customizer) + : undefined; + + if (guard && isIterateeCall(sources[0], sources[1], guard)) { + customizer = length < 3 ? undefined : customizer; + length = 1; + } + object = Object(object); + while (++index < length) { + var source = sources[index]; + if (source) { + assigner(object, source, index, customizer); + } + } + return object; + }); + } + + /** + * Creates a `baseEach` or `baseEachRight` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseEach(eachFunc, fromRight) { + return function(collection, iteratee) { + if (collection == null) { + return collection; + } + if (!isArrayLike(collection)) { + return eachFunc(collection, iteratee); + } + var length = collection.length, + index = fromRight ? length : -1, + iterable = Object(collection); + + while ((fromRight ? index-- : ++index < length)) { + if (iteratee(iterable[index], index, iterable) === false) { + break; + } + } + return collection; + }; + } + + /** + * Creates a base function for methods like `_.forIn` and `_.forOwn`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var index = -1, + iterable = Object(object), + props = keysFunc(object), + length = props.length; + + while (length--) { + var key = props[fromRight ? length : ++index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; + } + + /** + * Creates a function that wraps `func` to invoke it with the optional `this` + * binding of `thisArg`. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {*} [thisArg] The `this` binding of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createBind(func, bitmask, thisArg) { + var isBind = bitmask & WRAP_BIND_FLAG, + Ctor = createCtor(func); + + function wrapper() { + var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + return fn.apply(isBind ? thisArg : this, arguments); + } + return wrapper; + } + + /** + * Creates a function like `_.lowerFirst`. + * + * @private + * @param {string} methodName The name of the `String` case method to use. + * @returns {Function} Returns the new case function. + */ + function createCaseFirst(methodName) { + return function(string) { + string = toString(string); + + var strSymbols = hasUnicode(string) + ? stringToArray(string) + : undefined; + + var chr = strSymbols + ? strSymbols[0] + : string.charAt(0); + + var trailing = strSymbols + ? castSlice(strSymbols, 1).join('') + : string.slice(1); + + return chr[methodName]() + trailing; + }; + } + + /** + * Creates a function like `_.camelCase`. + * + * @private + * @param {Function} callback The function to combine each word. + * @returns {Function} Returns the new compounder function. + */ + function createCompounder(callback) { + return function(string) { + return arrayReduce(words(deburr(string).replace(reApos, '')), callback, ''); + }; + } + + /** + * Creates a function that produces an instance of `Ctor` regardless of + * whether it was invoked as part of a `new` expression or by `call` or `apply`. + * + * @private + * @param {Function} Ctor The constructor to wrap. + * @returns {Function} Returns the new wrapped function. + */ + function createCtor(Ctor) { + return function() { + // Use a `switch` statement to work with class constructors. See + // http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist + // for more details. + var args = arguments; + switch (args.length) { + case 0: return new Ctor; + case 1: return new Ctor(args[0]); + case 2: return new Ctor(args[0], args[1]); + case 3: return new Ctor(args[0], args[1], args[2]); + case 4: return new Ctor(args[0], args[1], args[2], args[3]); + case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]); + case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]); + case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); + } + var thisBinding = baseCreate(Ctor.prototype), + result = Ctor.apply(thisBinding, args); + + // Mimic the constructor's `return` behavior. + // See https://es5.github.io/#x13.2.2 for more details. + return isObject(result) ? result : thisBinding; + }; + } + + /** + * Creates a function that wraps `func` to enable currying. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {number} arity The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createCurry(func, bitmask, arity) { + var Ctor = createCtor(func); + + function wrapper() { + var length = arguments.length, + args = Array(length), + index = length, + placeholder = getHolder(wrapper); + + while (index--) { + args[index] = arguments[index]; + } + var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder) + ? [] + : replaceHolders(args, placeholder); + + length -= holders.length; + if (length < arity) { + return createRecurry( + func, bitmask, createHybrid, wrapper.placeholder, undefined, + args, holders, undefined, undefined, arity - length); + } + var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + return apply(fn, this, args); + } + return wrapper; + } + + /** + * Creates a `_.find` or `_.findLast` function. + * + * @private + * @param {Function} findIndexFunc The function to find the collection index. + * @returns {Function} Returns the new find function. + */ + function createFind(findIndexFunc) { + return function(collection, predicate, fromIndex) { + var iterable = Object(collection); + if (!isArrayLike(collection)) { + var iteratee = getIteratee(predicate, 3); + collection = keys(collection); + predicate = function(key) { return iteratee(iterable[key], key, iterable); }; + } + var index = findIndexFunc(collection, predicate, fromIndex); + return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined; + }; + } + + /** + * Creates a `_.flow` or `_.flowRight` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new flow function. + */ + function createFlow(fromRight) { + return flatRest(function(funcs) { + var length = funcs.length, + index = length, + prereq = LodashWrapper.prototype.thru; + + if (fromRight) { + funcs.reverse(); + } + while (index--) { + var func = funcs[index]; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (prereq && !wrapper && getFuncName(func) == 'wrapper') { + var wrapper = new LodashWrapper([], true); + } + } + index = wrapper ? index : length; + while (++index < length) { + func = funcs[index]; + + var funcName = getFuncName(func), + data = funcName == 'wrapper' ? getData(func) : undefined; + + if (data && isLaziable(data[0]) && + data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) && + !data[4].length && data[9] == 1 + ) { + wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); + } else { + wrapper = (func.length == 1 && isLaziable(func)) + ? wrapper[funcName]() + : wrapper.thru(func); + } + } + return function() { + var args = arguments, + value = args[0]; + + if (wrapper && args.length == 1 && isArray(value)) { + return wrapper.plant(value).value(); + } + var index = 0, + result = length ? funcs[index].apply(this, args) : value; + + while (++index < length) { + result = funcs[index].call(this, result); + } + return result; + }; + }); + } + + /** + * Creates a function that wraps `func` to invoke it with optional `this` + * binding of `thisArg`, partial application, and currying. + * + * @private + * @param {Function|string} func The function or method name to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to prepend to those provided to + * the new function. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [partialsRight] The arguments to append to those provided + * to the new function. + * @param {Array} [holdersRight] The `partialsRight` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { + var isAry = bitmask & WRAP_ARY_FLAG, + isBind = bitmask & WRAP_BIND_FLAG, + isBindKey = bitmask & WRAP_BIND_KEY_FLAG, + isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG), + isFlip = bitmask & WRAP_FLIP_FLAG, + Ctor = isBindKey ? undefined : createCtor(func); + + function wrapper() { + var length = arguments.length, + args = Array(length), + index = length; + + while (index--) { + args[index] = arguments[index]; + } + if (isCurried) { + var placeholder = getHolder(wrapper), + holdersCount = countHolders(args, placeholder); + } + if (partials) { + args = composeArgs(args, partials, holders, isCurried); + } + if (partialsRight) { + args = composeArgsRight(args, partialsRight, holdersRight, isCurried); + } + length -= holdersCount; + if (isCurried && length < arity) { + var newHolders = replaceHolders(args, placeholder); + return createRecurry( + func, bitmask, createHybrid, wrapper.placeholder, thisArg, + args, newHolders, argPos, ary, arity - length + ); + } + var thisBinding = isBind ? thisArg : this, + fn = isBindKey ? thisBinding[func] : func; + + length = args.length; + if (argPos) { + args = reorder(args, argPos); + } else if (isFlip && length > 1) { + args.reverse(); + } + if (isAry && ary < length) { + args.length = ary; + } + if (this && this !== root && this instanceof wrapper) { + fn = Ctor || createCtor(fn); + } + return fn.apply(thisBinding, args); + } + return wrapper; + } + + /** + * Creates a function like `_.invertBy`. + * + * @private + * @param {Function} setter The function to set accumulator values. + * @param {Function} toIteratee The function to resolve iteratees. + * @returns {Function} Returns the new inverter function. + */ + function createInverter(setter, toIteratee) { + return function(object, iteratee) { + return baseInverter(object, setter, toIteratee(iteratee), {}); + }; + } + + /** + * Creates a function that performs a mathematical operation on two values. + * + * @private + * @param {Function} operator The function to perform the operation. + * @param {number} [defaultValue] The value used for `undefined` arguments. + * @returns {Function} Returns the new mathematical operation function. + */ + function createMathOperation(operator, defaultValue) { + return function(value, other) { + var result; + if (value === undefined && other === undefined) { + return defaultValue; + } + if (value !== undefined) { + result = value; + } + if (other !== undefined) { + if (result === undefined) { + return other; + } + if (typeof value == 'string' || typeof other == 'string') { + value = baseToString(value); + other = baseToString(other); + } else { + value = baseToNumber(value); + other = baseToNumber(other); + } + result = operator(value, other); + } + return result; + }; + } + + /** + * Creates a function like `_.over`. + * + * @private + * @param {Function} arrayFunc The function to iterate over iteratees. + * @returns {Function} Returns the new over function. + */ + function createOver(arrayFunc) { + return flatRest(function(iteratees) { + iteratees = arrayMap(iteratees, baseUnary(getIteratee())); + return baseRest(function(args) { + var thisArg = this; + return arrayFunc(iteratees, function(iteratee) { + return apply(iteratee, thisArg, args); + }); + }); + }); + } + + /** + * Creates the padding for `string` based on `length`. The `chars` string + * is truncated if the number of characters exceeds `length`. + * + * @private + * @param {number} length The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padding for `string`. + */ + function createPadding(length, chars) { + chars = chars === undefined ? ' ' : baseToString(chars); + + var charsLength = chars.length; + if (charsLength < 2) { + return charsLength ? baseRepeat(chars, length) : chars; + } + var result = baseRepeat(chars, nativeCeil(length / stringSize(chars))); + return hasUnicode(chars) + ? castSlice(stringToArray(result), 0, length).join('') + : result.slice(0, length); + } + + /** + * Creates a function that wraps `func` to invoke it with the `this` binding + * of `thisArg` and `partials` prepended to the arguments it receives. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {*} thisArg The `this` binding of `func`. + * @param {Array} partials The arguments to prepend to those provided to + * the new function. + * @returns {Function} Returns the new wrapped function. + */ + function createPartial(func, bitmask, thisArg, partials) { + var isBind = bitmask & WRAP_BIND_FLAG, + Ctor = createCtor(func); + + function wrapper() { + var argsIndex = -1, + argsLength = arguments.length, + leftIndex = -1, + leftLength = partials.length, + args = Array(leftLength + argsLength), + fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + + while (++leftIndex < leftLength) { + args[leftIndex] = partials[leftIndex]; + } + while (argsLength--) { + args[leftIndex++] = arguments[++argsIndex]; + } + return apply(fn, isBind ? thisArg : this, args); + } + return wrapper; + } + + /** + * Creates a `_.range` or `_.rangeRight` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new range function. + */ + function createRange(fromRight) { + return function(start, end, step) { + if (step && typeof step != 'number' && isIterateeCall(start, end, step)) { + end = step = undefined; + } + // Ensure the sign of `-0` is preserved. + start = toFinite(start); + if (end === undefined) { + end = start; + start = 0; + } else { + end = toFinite(end); + } + step = step === undefined ? (start < end ? 1 : -1) : toFinite(step); + return baseRange(start, end, step, fromRight); + }; + } + + /** + * Creates a function that performs a relational operation on two values. + * + * @private + * @param {Function} operator The function to perform the operation. + * @returns {Function} Returns the new relational operation function. + */ + function createRelationalOperation(operator) { + return function(value, other) { + if (!(typeof value == 'string' && typeof other == 'string')) { + value = toNumber(value); + other = toNumber(other); + } + return operator(value, other); + }; + } + + /** + * Creates a function that wraps `func` to continue currying. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {Function} wrapFunc The function to create the `func` wrapper. + * @param {*} placeholder The placeholder value. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to prepend to those provided to + * the new function. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) { + var isCurry = bitmask & WRAP_CURRY_FLAG, + newHolders = isCurry ? holders : undefined, + newHoldersRight = isCurry ? undefined : holders, + newPartials = isCurry ? partials : undefined, + newPartialsRight = isCurry ? undefined : partials; + + bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG); + bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG); + + if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) { + bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG); + } + var newData = [ + func, bitmask, thisArg, newPartials, newHolders, newPartialsRight, + newHoldersRight, argPos, ary, arity + ]; + + var result = wrapFunc.apply(undefined, newData); + if (isLaziable(func)) { + setData(result, newData); + } + result.placeholder = placeholder; + return setWrapToString(result, func, bitmask); + } + + /** + * Creates a function like `_.round`. + * + * @private + * @param {string} methodName The name of the `Math` method to use when rounding. + * @returns {Function} Returns the new round function. + */ + function createRound(methodName) { + var func = Math[methodName]; + return function(number, precision) { + number = toNumber(number); + precision = precision == null ? 0 : nativeMin(toInteger(precision), 292); + if (precision) { + // Shift with exponential notation to avoid floating-point issues. + // See [MDN](https://mdn.io/round#Examples) for more details. + var pair = (toString(number) + 'e').split('e'), + value = func(pair[0] + 'e' + (+pair[1] + precision)); + + pair = (toString(value) + 'e').split('e'); + return +(pair[0] + 'e' + (+pair[1] - precision)); + } + return func(number); + }; + } + + /** + * Creates a set object of `values`. + * + * @private + * @param {Array} values The values to add to the set. + * @returns {Object} Returns the new set. + */ + var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) { + return new Set(values); + }; + + /** + * Creates a `_.toPairs` or `_.toPairsIn` function. + * + * @private + * @param {Function} keysFunc The function to get the keys of a given object. + * @returns {Function} Returns the new pairs function. + */ + function createToPairs(keysFunc) { + return function(object) { + var tag = getTag(object); + if (tag == mapTag) { + return mapToArray(object); + } + if (tag == setTag) { + return setToPairs(object); + } + return baseToPairs(object, keysFunc(object)); + }; + } + + /** + * Creates a function that either curries or invokes `func` with optional + * `this` binding and partially applied arguments. + * + * @private + * @param {Function|string} func The function or method name to wrap. + * @param {number} bitmask The bitmask flags. + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` or `_.curryRight` of a bound function + * 8 - `_.curry` + * 16 - `_.curryRight` + * 32 - `_.partial` + * 64 - `_.partialRight` + * 128 - `_.rearg` + * 256 - `_.ary` + * 512 - `_.flip` + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to be partially applied. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { + var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; + if (!isBindKey && typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + var length = partials ? partials.length : 0; + if (!length) { + bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); + partials = holders = undefined; + } + ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0); + arity = arity === undefined ? arity : toInteger(arity); + length -= holders ? holders.length : 0; + + if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) { + var partialsRight = partials, + holdersRight = holders; + + partials = holders = undefined; + } + var data = isBindKey ? undefined : getData(func); + + var newData = [ + func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, + argPos, ary, arity + ]; + + if (data) { + mergeData(newData, data); + } + func = newData[0]; + bitmask = newData[1]; + thisArg = newData[2]; + partials = newData[3]; + holders = newData[4]; + arity = newData[9] = newData[9] === undefined + ? (isBindKey ? 0 : func.length) + : nativeMax(newData[9] - length, 0); + + if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) { + bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG); + } + if (!bitmask || bitmask == WRAP_BIND_FLAG) { + var result = createBind(func, bitmask, thisArg); + } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) { + result = createCurry(func, bitmask, arity); + } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) { + result = createPartial(func, bitmask, thisArg, partials); + } else { + result = createHybrid.apply(undefined, newData); + } + var setter = data ? baseSetData : setData; + return setWrapToString(setter(result, newData), func, bitmask); + } + + /** + * Used by `_.defaults` to customize its `_.assignIn` use to assign properties + * of source objects to the destination object for all destination properties + * that resolve to `undefined`. + * + * @private + * @param {*} objValue The destination value. + * @param {*} srcValue The source value. + * @param {string} key The key of the property to assign. + * @param {Object} object The parent object of `objValue`. + * @returns {*} Returns the value to assign. + */ + function customDefaultsAssignIn(objValue, srcValue, key, object) { + if (objValue === undefined || + (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) { + return srcValue; + } + return objValue; + } + + /** + * Used by `_.defaultsDeep` to customize its `_.merge` use to merge source + * objects into destination objects that are passed thru. + * + * @private + * @param {*} objValue The destination value. + * @param {*} srcValue The source value. + * @param {string} key The key of the property to merge. + * @param {Object} object The parent object of `objValue`. + * @param {Object} source The parent object of `srcValue`. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + * @returns {*} Returns the value to assign. + */ + function customDefaultsMerge(objValue, srcValue, key, object, source, stack) { + if (isObject(objValue) && isObject(srcValue)) { + // Recursively merge objects and arrays (susceptible to call stack limits). + stack.set(srcValue, objValue); + baseMerge(objValue, srcValue, undefined, customDefaultsMerge, stack); + stack['delete'](srcValue); + } + return objValue; + } + + /** + * Used by `_.omit` to customize its `_.cloneDeep` use to only clone plain + * objects. + * + * @private + * @param {*} value The value to inspect. + * @param {string} key The key of the property to inspect. + * @returns {*} Returns the uncloned value or `undefined` to defer cloning to `_.cloneDeep`. + */ + function customOmitClone(value) { + return isPlainObject(value) ? undefined : value; + } + + /** + * A specialized version of `baseIsEqualDeep` for arrays with support for + * partial deep comparisons. + * + * @private + * @param {Array} array The array to compare. + * @param {Array} other The other array to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `array` and `other` objects. + * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. + */ + function equalArrays(array, other, bitmask, customizer, equalFunc, stack) { + var isPartial = bitmask & COMPARE_PARTIAL_FLAG, + arrLength = array.length, + othLength = other.length; + + if (arrLength != othLength && !(isPartial && othLength > arrLength)) { + return false; + } + // Assume cyclic values are equal. + var stacked = stack.get(array); + if (stacked && stack.get(other)) { + return stacked == other; + } + var index = -1, + result = true, + seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined; + + stack.set(array, other); + stack.set(other, array); + + // Ignore non-index properties. + while (++index < arrLength) { + var arrValue = array[index], + othValue = other[index]; + + if (customizer) { + var compared = isPartial + ? customizer(othValue, arrValue, index, other, array, stack) + : customizer(arrValue, othValue, index, array, other, stack); + } + if (compared !== undefined) { + if (compared) { + continue; + } + result = false; + break; + } + // Recursively compare arrays (susceptible to call stack limits). + if (seen) { + if (!arraySome(other, function(othValue, othIndex) { + if (!cacheHas(seen, othIndex) && + (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) { + return seen.push(othIndex); + } + })) { + result = false; + break; + } + } else if (!( + arrValue === othValue || + equalFunc(arrValue, othValue, bitmask, customizer, stack) + )) { + result = false; + break; + } + } + stack['delete'](array); + stack['delete'](other); + return result; + } + + /** + * A specialized version of `baseIsEqualDeep` for comparing objects of + * the same `toStringTag`. + * + * **Note:** This function only supports comparing values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {string} tag The `toStringTag` of the objects to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) { + switch (tag) { + case dataViewTag: + if ((object.byteLength != other.byteLength) || + (object.byteOffset != other.byteOffset)) { + return false; + } + object = object.buffer; + other = other.buffer; + + case arrayBufferTag: + if ((object.byteLength != other.byteLength) || + !equalFunc(new Uint8Array(object), new Uint8Array(other))) { + return false; + } + return true; + + case boolTag: + case dateTag: + case numberTag: + // Coerce booleans to `1` or `0` and dates to milliseconds. + // Invalid dates are coerced to `NaN`. + return eq(+object, +other); + + case errorTag: + return object.name == other.name && object.message == other.message; + + case regexpTag: + case stringTag: + // Coerce regexes to strings and treat strings, primitives and objects, + // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring + // for more details. + return object == (other + ''); + + case mapTag: + var convert = mapToArray; + + case setTag: + var isPartial = bitmask & COMPARE_PARTIAL_FLAG; + convert || (convert = setToArray); + + if (object.size != other.size && !isPartial) { + return false; + } + // Assume cyclic values are equal. + var stacked = stack.get(object); + if (stacked) { + return stacked == other; + } + bitmask |= COMPARE_UNORDERED_FLAG; + + // Recursively compare objects (susceptible to call stack limits). + stack.set(object, other); + var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack); + stack['delete'](object); + return result; + + case symbolTag: + if (symbolValueOf) { + return symbolValueOf.call(object) == symbolValueOf.call(other); + } + } + return false; + } + + /** + * A specialized version of `baseIsEqualDeep` for objects with support for + * partial deep comparisons. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function equalObjects(object, other, bitmask, customizer, equalFunc, stack) { + var isPartial = bitmask & COMPARE_PARTIAL_FLAG, + objProps = getAllKeys(object), + objLength = objProps.length, + othProps = getAllKeys(other), + othLength = othProps.length; + + if (objLength != othLength && !isPartial) { + return false; + } + var index = objLength; + while (index--) { + var key = objProps[index]; + if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) { + return false; + } + } + // Assume cyclic values are equal. + var stacked = stack.get(object); + if (stacked && stack.get(other)) { + return stacked == other; + } + var result = true; + stack.set(object, other); + stack.set(other, object); + + var skipCtor = isPartial; + while (++index < objLength) { + key = objProps[index]; + var objValue = object[key], + othValue = other[key]; + + if (customizer) { + var compared = isPartial + ? customizer(othValue, objValue, key, other, object, stack) + : customizer(objValue, othValue, key, object, other, stack); + } + // Recursively compare objects (susceptible to call stack limits). + if (!(compared === undefined + ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack)) + : compared + )) { + result = false; + break; + } + skipCtor || (skipCtor = key == 'constructor'); + } + if (result && !skipCtor) { + var objCtor = object.constructor, + othCtor = other.constructor; + + // Non `Object` object instances with different constructors are not equal. + if (objCtor != othCtor && + ('constructor' in object && 'constructor' in other) && + !(typeof objCtor == 'function' && objCtor instanceof objCtor && + typeof othCtor == 'function' && othCtor instanceof othCtor)) { + result = false; + } + } + stack['delete'](object); + stack['delete'](other); + return result; + } + + /** + * A specialized version of `baseRest` which flattens the rest array. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @returns {Function} Returns the new function. + */ + function flatRest(func) { + return setToString(overRest(func, undefined, flatten), func + ''); + } + + /** + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ + function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); + } + + /** + * Creates an array of own and inherited enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ + function getAllKeysIn(object) { + return baseGetAllKeys(object, keysIn, getSymbolsIn); + } + + /** + * Gets metadata for `func`. + * + * @private + * @param {Function} func The function to query. + * @returns {*} Returns the metadata for `func`. + */ + var getData = !metaMap ? noop : function(func) { + return metaMap.get(func); + }; + + /** + * Gets the name of `func`. + * + * @private + * @param {Function} func The function to query. + * @returns {string} Returns the function name. + */ + function getFuncName(func) { + var result = (func.name + ''), + array = realNames[result], + length = hasOwnProperty.call(realNames, result) ? array.length : 0; + + while (length--) { + var data = array[length], + otherFunc = data.func; + if (otherFunc == null || otherFunc == func) { + return data.name; + } + } + return result; + } + + /** + * Gets the argument placeholder value for `func`. + * + * @private + * @param {Function} func The function to inspect. + * @returns {*} Returns the placeholder value. + */ + function getHolder(func) { + var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func; + return object.placeholder; + } + + /** + * Gets the appropriate "iteratee" function. If `_.iteratee` is customized, + * this function returns the custom method, otherwise it returns `baseIteratee`. + * If arguments are provided, the chosen function is invoked with them and + * its result is returned. + * + * @private + * @param {*} [value] The value to convert to an iteratee. + * @param {number} [arity] The arity of the created iteratee. + * @returns {Function} Returns the chosen function or its result. + */ + function getIteratee() { + var result = lodash.iteratee || iteratee; + result = result === iteratee ? baseIteratee : result; + return arguments.length ? result(arguments[0], arguments[1]) : result; + } + + /** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ + function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; + } + + /** + * Gets the property names, values, and compare flags of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the match data of `object`. + */ + function getMatchData(object) { + var result = keys(object), + length = result.length; + + while (length--) { + var key = result[length], + value = object[key]; + + result[length] = [key, value, isStrictComparable(value)]; + } + return result; + } + + /** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ + function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; + } + + /** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ + function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; + } + + /** + * Creates an array of the own enumerable symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ + var getSymbols = !nativeGetSymbols ? stubArray : function(object) { + if (object == null) { + return []; + } + object = Object(object); + return arrayFilter(nativeGetSymbols(object), function(symbol) { + return propertyIsEnumerable.call(object, symbol); + }); + }; + + /** + * Creates an array of the own and inherited enumerable symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ + var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) { + var result = []; + while (object) { + arrayPush(result, getSymbols(object)); + object = getPrototype(object); + } + return result; + }; + + /** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + var getTag = baseGetTag; + + // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6. + if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map && getTag(new Map) != mapTag) || + (Promise && getTag(Promise.resolve()) != promiseTag) || + (Set && getTag(new Set) != setTag) || + (WeakMap && getTag(new WeakMap) != weakMapTag)) { + getTag = function(value) { + var result = baseGetTag(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : ''; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; + } + + /** + * Gets the view, applying any `transforms` to the `start` and `end` positions. + * + * @private + * @param {number} start The start of the view. + * @param {number} end The end of the view. + * @param {Array} transforms The transformations to apply to the view. + * @returns {Object} Returns an object containing the `start` and `end` + * positions of the view. + */ + function getView(start, end, transforms) { + var index = -1, + length = transforms.length; + + while (++index < length) { + var data = transforms[index], + size = data.size; + + switch (data.type) { + case 'drop': start += size; break; + case 'dropRight': end -= size; break; + case 'take': end = nativeMin(end, start + size); break; + case 'takeRight': start = nativeMax(start, end - size); break; + } + } + return { 'start': start, 'end': end }; + } + + /** + * Extracts wrapper details from the `source` body comment. + * + * @private + * @param {string} source The source to inspect. + * @returns {Array} Returns the wrapper details. + */ + function getWrapDetails(source) { + var match = source.match(reWrapDetails); + return match ? match[1].split(reSplitDetails) : []; + } + + /** + * Checks if `path` exists on `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @param {Function} hasFunc The function to check properties. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + */ + function hasPath(object, path, hasFunc) { + path = castPath(path, object); + + var index = -1, + length = path.length, + result = false; + + while (++index < length) { + var key = toKey(path[index]); + if (!(result = object != null && hasFunc(object, key))) { + break; + } + object = object[key]; + } + if (result || ++index != length) { + return result; + } + length = object == null ? 0 : object.length; + return !!length && isLength(length) && isIndex(key, length) && + (isArray(object) || isArguments(object)); + } + + /** + * Initializes an array clone. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the initialized clone. + */ + function initCloneArray(array) { + var length = array.length, + result = new array.constructor(length); + + // Add properties assigned by `RegExp#exec`. + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; + } + + /** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; + } + + /** + * Initializes an object clone based on its `toStringTag`. + * + * **Note:** This function only supports cloning values with tags of + * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`. + * + * @private + * @param {Object} object The object to clone. + * @param {string} tag The `toStringTag` of the object to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneByTag(object, tag, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return cloneArrayBuffer(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case dataViewTag: + return cloneDataView(object, isDeep); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + return cloneTypedArray(object, isDeep); + + case mapTag: + return new Ctor; + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + return cloneRegExp(object); + + case setTag: + return new Ctor; + + case symbolTag: + return cloneSymbol(object); + } + } + + /** + * Inserts wrapper `details` in a comment at the top of the `source` body. + * + * @private + * @param {string} source The source to modify. + * @returns {Array} details The details to insert. + * @returns {string} Returns the modified source. + */ + function insertWrapDetails(source, details) { + var length = details.length; + if (!length) { + return source; + } + var lastIndex = length - 1; + details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex]; + details = details.join(length > 2 ? ', ' : ' '); + return source.replace(reWrapComment, '{\n/* [wrapped with ' + details + '] */\n'); + } + + /** + * Checks if `value` is a flattenable `arguments` object or array. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is flattenable, else `false`. + */ + function isFlattenable(value) { + return isArray(value) || isArguments(value) || + !!(spreadableSymbol && value && value[spreadableSymbol]); + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex(value, length) { + var type = typeof value; + length = length == null ? MAX_SAFE_INTEGER : length; + + return !!length && + (type == 'number' || + (type != 'symbol' && reIsUint.test(value))) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if the given arguments are from an iteratee call. + * + * @private + * @param {*} value The potential iteratee value argument. + * @param {*} index The potential iteratee index or key argument. + * @param {*} object The potential iteratee object argument. + * @returns {boolean} Returns `true` if the arguments are from an iteratee call, + * else `false`. + */ + function isIterateeCall(value, index, object) { + if (!isObject(object)) { + return false; + } + var type = typeof index; + if (type == 'number' + ? (isArrayLike(object) && isIndex(index, object.length)) + : (type == 'string' && index in object) + ) { + return eq(object[index], value); + } + return false; + } + + /** + * Checks if `value` is a property name and not a property path. + * + * @private + * @param {*} value The value to check. + * @param {Object} [object] The object to query keys on. + * @returns {boolean} Returns `true` if `value` is a property name, else `false`. + */ + function isKey(value, object) { + if (isArray(value)) { + return false; + } + var type = typeof value; + if (type == 'number' || type == 'symbol' || type == 'boolean' || + value == null || isSymbol(value)) { + return true; + } + return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || + (object != null && value in Object(object)); + } + + /** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ + function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); + } + + /** + * Checks if `func` has a lazy counterpart. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` has a lazy counterpart, + * else `false`. + */ + function isLaziable(func) { + var funcName = getFuncName(func), + other = lodash[funcName]; + + if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) { + return false; + } + if (func === other) { + return true; + } + var data = getData(other); + return !!data && func === data[0]; + } + + /** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ + function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); + } + + /** + * Checks if `func` is capable of being masked. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `func` is maskable, else `false`. + */ + var isMaskable = coreJsData ? isFunction : stubFalse; + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; + + return value === proto; + } + + /** + * Checks if `value` is suitable for strict equality comparisons, i.e. `===`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` if suitable for strict + * equality comparisons, else `false`. + */ + function isStrictComparable(value) { + return value === value && !isObject(value); + } + + /** + * A specialized version of `matchesProperty` for source values suitable + * for strict equality comparisons, i.e. `===`. + * + * @private + * @param {string} key The key of the property to get. + * @param {*} srcValue The value to match. + * @returns {Function} Returns the new spec function. + */ + function matchesStrictComparable(key, srcValue) { + return function(object) { + if (object == null) { + return false; + } + return object[key] === srcValue && + (srcValue !== undefined || (key in Object(object))); + }; + } + + /** + * A specialized version of `_.memoize` which clears the memoized function's + * cache when it exceeds `MAX_MEMOIZE_SIZE`. + * + * @private + * @param {Function} func The function to have its output memoized. + * @returns {Function} Returns the new memoized function. + */ + function memoizeCapped(func) { + var result = memoize(func, function(key) { + if (cache.size === MAX_MEMOIZE_SIZE) { + cache.clear(); + } + return key; + }); + + var cache = result.cache; + return result; + } + + /** + * Merges the function metadata of `source` into `data`. + * + * Merging metadata reduces the number of wrappers used to invoke a function. + * This is possible because methods like `_.bind`, `_.curry`, and `_.partial` + * may be applied regardless of execution order. Methods like `_.ary` and + * `_.rearg` modify function arguments, making the order in which they are + * executed important, preventing the merging of metadata. However, we make + * an exception for a safe combined case where curried functions have `_.ary` + * and or `_.rearg` applied. + * + * @private + * @param {Array} data The destination metadata. + * @param {Array} source The source metadata. + * @returns {Array} Returns `data`. + */ + function mergeData(data, source) { + var bitmask = data[1], + srcBitmask = source[1], + newBitmask = bitmask | srcBitmask, + isCommon = newBitmask < (WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG | WRAP_ARY_FLAG); + + var isCombo = + ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_CURRY_FLAG)) || + ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_REARG_FLAG) && (data[7].length <= source[8])) || + ((srcBitmask == (WRAP_ARY_FLAG | WRAP_REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == WRAP_CURRY_FLAG)); + + // Exit early if metadata can't be merged. + if (!(isCommon || isCombo)) { + return data; + } + // Use source `thisArg` if available. + if (srcBitmask & WRAP_BIND_FLAG) { + data[2] = source[2]; + // Set when currying a bound function. + newBitmask |= bitmask & WRAP_BIND_FLAG ? 0 : WRAP_CURRY_BOUND_FLAG; + } + // Compose partial arguments. + var value = source[3]; + if (value) { + var partials = data[3]; + data[3] = partials ? composeArgs(partials, value, source[4]) : value; + data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4]; + } + // Compose partial right arguments. + value = source[5]; + if (value) { + partials = data[5]; + data[5] = partials ? composeArgsRight(partials, value, source[6]) : value; + data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6]; + } + // Use source `argPos` if available. + value = source[7]; + if (value) { + data[7] = value; + } + // Use source `ary` if it's smaller. + if (srcBitmask & WRAP_ARY_FLAG) { + data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]); + } + // Use source `arity` if one is not provided. + if (data[9] == null) { + data[9] = source[9]; + } + // Use source `func` and merge bitmasks. + data[0] = source[0]; + data[1] = newBitmask; + + return data; + } + + /** + * This function is like + * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * except that it includes inherited enumerable properties. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function nativeKeysIn(object) { + var result = []; + if (object != null) { + for (var key in Object(object)) { + result.push(key); + } + } + return result; + } + + /** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ + function objectToString(value) { + return nativeObjectToString.call(value); + } + + /** + * A specialized version of `baseRest` which transforms the rest array. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @param {Function} transform The rest array transform. + * @returns {Function} Returns the new function. + */ + function overRest(func, start, transform) { + start = nativeMax(start === undefined ? (func.length - 1) : start, 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + array = Array(length); + + while (++index < length) { + array[index] = args[start + index]; + } + index = -1; + var otherArgs = Array(start + 1); + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = transform(array); + return apply(func, this, otherArgs); + }; + } + + /** + * Gets the parent value at `path` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} path The path to get the parent value of. + * @returns {*} Returns the parent value. + */ + function parent(object, path) { + return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1)); + } + + /** + * Reorder `array` according to the specified indexes where the element at + * the first index is assigned as the first element, the element at + * the second index is assigned as the second element, and so on. + * + * @private + * @param {Array} array The array to reorder. + * @param {Array} indexes The arranged array indexes. + * @returns {Array} Returns `array`. + */ + function reorder(array, indexes) { + var arrLength = array.length, + length = nativeMin(indexes.length, arrLength), + oldArray = copyArray(array); + + while (length--) { + var index = indexes[length]; + array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined; + } + return array; + } + + /** + * Sets metadata for `func`. + * + * **Note:** If this function becomes hot, i.e. is invoked a lot in a short + * period of time, it will trip its breaker and transition to an identity + * function to avoid garbage collection pauses in V8. See + * [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070) + * for more details. + * + * @private + * @param {Function} func The function to associate metadata with. + * @param {*} data The metadata. + * @returns {Function} Returns `func`. + */ + var setData = shortOut(baseSetData); + + /** + * A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout). + * + * @private + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @returns {number|Object} Returns the timer id or timeout object. + */ + var setTimeout = ctxSetTimeout || function(func, wait) { + return root.setTimeout(func, wait); + }; + + /** + * Sets the `toString` method of `func` to return `string`. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ + var setToString = shortOut(baseSetToString); + + /** + * Sets the `toString` method of `wrapper` to mimic the source of `reference` + * with wrapper details in a comment at the top of the source body. + * + * @private + * @param {Function} wrapper The function to modify. + * @param {Function} reference The reference function. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @returns {Function} Returns `wrapper`. + */ + function setWrapToString(wrapper, reference, bitmask) { + var source = (reference + ''); + return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask))); + } + + /** + * Creates a function that'll short out and invoke `identity` instead + * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN` + * milliseconds. + * + * @private + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new shortable function. + */ + function shortOut(func) { + var count = 0, + lastCalled = 0; + + return function() { + var stamp = nativeNow(), + remaining = HOT_SPAN - (stamp - lastCalled); + + lastCalled = stamp; + if (remaining > 0) { + if (++count >= HOT_COUNT) { + return arguments[0]; + } + } else { + count = 0; + } + return func.apply(undefined, arguments); + }; + } + + /** + * A specialized version of `_.shuffle` which mutates and sets the size of `array`. + * + * @private + * @param {Array} array The array to shuffle. + * @param {number} [size=array.length] The size of `array`. + * @returns {Array} Returns `array`. + */ + function shuffleSelf(array, size) { + var index = -1, + length = array.length, + lastIndex = length - 1; + + size = size === undefined ? length : size; + while (++index < size) { + var rand = baseRandom(index, lastIndex), + value = array[rand]; + + array[rand] = array[index]; + array[index] = value; + } + array.length = size; + return array; + } + + /** + * Converts `string` to a property path array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the property path array. + */ + var stringToPath = memoizeCapped(function(string) { + var result = []; + if (string.charCodeAt(0) === 46 /* . */) { + result.push(''); + } + string.replace(rePropName, function(match, number, quote, subString) { + result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match)); + }); + return result; + }); + + /** + * Converts `value` to a string key if it's not a string or symbol. + * + * @private + * @param {*} value The value to inspect. + * @returns {string|symbol} Returns the key. + */ + function toKey(value) { + if (typeof value == 'string' || isSymbol(value)) { + return value; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; + } + + /** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to convert. + * @returns {string} Returns the source code. + */ + function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; + } + + /** + * Updates wrapper `details` based on `bitmask` flags. + * + * @private + * @returns {Array} details The details to modify. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @returns {Array} Returns `details`. + */ + function updateWrapDetails(details, bitmask) { + arrayEach(wrapFlags, function(pair) { + var value = '_.' + pair[0]; + if ((bitmask & pair[1]) && !arrayIncludes(details, value)) { + details.push(value); + } + }); + return details.sort(); + } + + /** + * Creates a clone of `wrapper`. + * + * @private + * @param {Object} wrapper The wrapper to clone. + * @returns {Object} Returns the cloned wrapper. + */ + function wrapperClone(wrapper) { + if (wrapper instanceof LazyWrapper) { + return wrapper.clone(); + } + var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__); + result.__actions__ = copyArray(wrapper.__actions__); + result.__index__ = wrapper.__index__; + result.__values__ = wrapper.__values__; + return result; + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates an array of elements split into groups the length of `size`. + * If `array` can't be split evenly, the final chunk will be the remaining + * elements. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to process. + * @param {number} [size=1] The length of each chunk + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the new array of chunks. + * @example + * + * _.chunk(['a', 'b', 'c', 'd'], 2); + * // => [['a', 'b'], ['c', 'd']] + * + * _.chunk(['a', 'b', 'c', 'd'], 3); + * // => [['a', 'b', 'c'], ['d']] + */ + function chunk(array, size, guard) { + if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) { + size = 1; + } else { + size = nativeMax(toInteger(size), 0); + } + var length = array == null ? 0 : array.length; + if (!length || size < 1) { + return []; + } + var index = 0, + resIndex = 0, + result = Array(nativeCeil(length / size)); + + while (index < length) { + result[resIndex++] = baseSlice(array, index, (index += size)); + } + return result; + } + + /** + * Creates an array with all falsey values removed. The values `false`, `null`, + * `0`, `""`, `undefined`, and `NaN` are falsey. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to compact. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.compact([0, 1, false, 2, '', 3]); + * // => [1, 2, 3] + */ + function compact(array) { + var index = -1, + length = array == null ? 0 : array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value) { + result[resIndex++] = value; + } + } + return result; + } + + /** + * Creates a new array concatenating `array` with any additional arrays + * and/or values. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to concatenate. + * @param {...*} [values] The values to concatenate. + * @returns {Array} Returns the new concatenated array. + * @example + * + * var array = [1]; + * var other = _.concat(array, 2, [3], [[4]]); + * + * console.log(other); + * // => [1, 2, 3, [4]] + * + * console.log(array); + * // => [1] + */ + function concat() { + var length = arguments.length; + if (!length) { + return []; + } + var args = Array(length - 1), + array = arguments[0], + index = length; + + while (index--) { + args[index - 1] = arguments[index]; + } + return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1)); + } + + /** + * Creates an array of `array` values not included in the other given arrays + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. The order and references of result values are + * determined by the first array. + * + * **Note:** Unlike `_.pullAll`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...Array} [values] The values to exclude. + * @returns {Array} Returns the new array of filtered values. + * @see _.without, _.xor + * @example + * + * _.difference([2, 1], [2, 3]); + * // => [1] + */ + var difference = baseRest(function(array, values) { + return isArrayLikeObject(array) + ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true)) + : []; + }); + + /** + * This method is like `_.difference` except that it accepts `iteratee` which + * is invoked for each element of `array` and `values` to generate the criterion + * by which they're compared. The order and references of result values are + * determined by the first array. The iteratee is invoked with one argument: + * (value). + * + * **Note:** Unlike `_.pullAllBy`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...Array} [values] The values to exclude. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); + * // => [1.2] + * + * // The `_.property` iteratee shorthand. + * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x'); + * // => [{ 'x': 2 }] + */ + var differenceBy = baseRest(function(array, values) { + var iteratee = last(values); + if (isArrayLikeObject(iteratee)) { + iteratee = undefined; + } + return isArrayLikeObject(array) + ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)) + : []; + }); + + /** + * This method is like `_.difference` except that it accepts `comparator` + * which is invoked to compare elements of `array` to `values`. The order and + * references of result values are determined by the first array. The comparator + * is invoked with two arguments: (arrVal, othVal). + * + * **Note:** Unlike `_.pullAllWith`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...Array} [values] The values to exclude. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * + * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual); + * // => [{ 'x': 2, 'y': 1 }] + */ + var differenceWith = baseRest(function(array, values) { + var comparator = last(values); + if (isArrayLikeObject(comparator)) { + comparator = undefined; + } + return isArrayLikeObject(array) + ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator) + : []; + }); + + /** + * Creates a slice of `array` with `n` elements dropped from the beginning. + * + * @static + * @memberOf _ + * @since 0.5.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to drop. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.drop([1, 2, 3]); + * // => [2, 3] + * + * _.drop([1, 2, 3], 2); + * // => [3] + * + * _.drop([1, 2, 3], 5); + * // => [] + * + * _.drop([1, 2, 3], 0); + * // => [1, 2, 3] + */ + function drop(array, n, guard) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + return baseSlice(array, n < 0 ? 0 : n, length); + } + + /** + * Creates a slice of `array` with `n` elements dropped from the end. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to drop. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.dropRight([1, 2, 3]); + * // => [1, 2] + * + * _.dropRight([1, 2, 3], 2); + * // => [1] + * + * _.dropRight([1, 2, 3], 5); + * // => [] + * + * _.dropRight([1, 2, 3], 0); + * // => [1, 2, 3] + */ + function dropRight(array, n, guard) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + n = length - n; + return baseSlice(array, 0, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` excluding elements dropped from the end. + * Elements are dropped until `predicate` returns falsey. The predicate is + * invoked with three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * _.dropRightWhile(users, function(o) { return !o.active; }); + * // => objects for ['barney'] + * + * // The `_.matches` iteratee shorthand. + * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false }); + * // => objects for ['barney', 'fred'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.dropRightWhile(users, ['active', false]); + * // => objects for ['barney'] + * + * // The `_.property` iteratee shorthand. + * _.dropRightWhile(users, 'active'); + * // => objects for ['barney', 'fred', 'pebbles'] + */ + function dropRightWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3), true, true) + : []; + } + + /** + * Creates a slice of `array` excluding elements dropped from the beginning. + * Elements are dropped until `predicate` returns falsey. The predicate is + * invoked with three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * _.dropWhile(users, function(o) { return !o.active; }); + * // => objects for ['pebbles'] + * + * // The `_.matches` iteratee shorthand. + * _.dropWhile(users, { 'user': 'barney', 'active': false }); + * // => objects for ['fred', 'pebbles'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.dropWhile(users, ['active', false]); + * // => objects for ['pebbles'] + * + * // The `_.property` iteratee shorthand. + * _.dropWhile(users, 'active'); + * // => objects for ['barney', 'fred', 'pebbles'] + */ + function dropWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3), true) + : []; + } + + /** + * Fills elements of `array` with `value` from `start` up to, but not + * including, `end`. + * + * **Note:** This method mutates `array`. + * + * @static + * @memberOf _ + * @since 3.2.0 + * @category Array + * @param {Array} array The array to fill. + * @param {*} value The value to fill `array` with. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3]; + * + * _.fill(array, 'a'); + * console.log(array); + * // => ['a', 'a', 'a'] + * + * _.fill(Array(3), 2); + * // => [2, 2, 2] + * + * _.fill([4, 6, 8, 10], '*', 1, 3); + * // => [4, '*', '*', 10] + */ + function fill(array, value, start, end) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + if (start && typeof start != 'number' && isIterateeCall(array, value, start)) { + start = 0; + end = length; + } + return baseFill(array, value, start, end); + } + + /** + * This method is like `_.find` except that it returns the index of the first + * element `predicate` returns truthy for instead of the element itself. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=0] The index to search from. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * _.findIndex(users, function(o) { return o.user == 'barney'; }); + * // => 0 + * + * // The `_.matches` iteratee shorthand. + * _.findIndex(users, { 'user': 'fred', 'active': false }); + * // => 1 + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findIndex(users, ['active', false]); + * // => 0 + * + * // The `_.property` iteratee shorthand. + * _.findIndex(users, 'active'); + * // => 2 + */ + function findIndex(array, predicate, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = fromIndex == null ? 0 : toInteger(fromIndex); + if (index < 0) { + index = nativeMax(length + index, 0); + } + return baseFindIndex(array, getIteratee(predicate, 3), index); + } + + /** + * This method is like `_.findIndex` except that it iterates over elements + * of `collection` from right to left. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=array.length-1] The index to search from. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; }); + * // => 2 + * + * // The `_.matches` iteratee shorthand. + * _.findLastIndex(users, { 'user': 'barney', 'active': true }); + * // => 0 + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findLastIndex(users, ['active', false]); + * // => 2 + * + * // The `_.property` iteratee shorthand. + * _.findLastIndex(users, 'active'); + * // => 0 + */ + function findLastIndex(array, predicate, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = length - 1; + if (fromIndex !== undefined) { + index = toInteger(fromIndex); + index = fromIndex < 0 + ? nativeMax(length + index, 0) + : nativeMin(index, length - 1); + } + return baseFindIndex(array, getIteratee(predicate, 3), index, true); + } + + /** + * Flattens `array` a single level deep. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to flatten. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flatten([1, [2, [3, [4]], 5]]); + * // => [1, 2, [3, [4]], 5] + */ + function flatten(array) { + var length = array == null ? 0 : array.length; + return length ? baseFlatten(array, 1) : []; + } + + /** + * Recursively flattens `array`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to flatten. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flattenDeep([1, [2, [3, [4]], 5]]); + * // => [1, 2, 3, 4, 5] + */ + function flattenDeep(array) { + var length = array == null ? 0 : array.length; + return length ? baseFlatten(array, INFINITY) : []; + } + + /** + * Recursively flatten `array` up to `depth` times. + * + * @static + * @memberOf _ + * @since 4.4.0 + * @category Array + * @param {Array} array The array to flatten. + * @param {number} [depth=1] The maximum recursion depth. + * @returns {Array} Returns the new flattened array. + * @example + * + * var array = [1, [2, [3, [4]], 5]]; + * + * _.flattenDepth(array, 1); + * // => [1, 2, [3, [4]], 5] + * + * _.flattenDepth(array, 2); + * // => [1, 2, 3, [4], 5] + */ + function flattenDepth(array, depth) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + depth = depth === undefined ? 1 : toInteger(depth); + return baseFlatten(array, depth); + } + + /** + * The inverse of `_.toPairs`; this method returns an object composed + * from key-value `pairs`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} pairs The key-value pairs. + * @returns {Object} Returns the new object. + * @example + * + * _.fromPairs([['a', 1], ['b', 2]]); + * // => { 'a': 1, 'b': 2 } + */ + function fromPairs(pairs) { + var index = -1, + length = pairs == null ? 0 : pairs.length, + result = {}; + + while (++index < length) { + var pair = pairs[index]; + result[pair[0]] = pair[1]; + } + return result; + } + + /** + * Gets the first element of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @alias first + * @category Array + * @param {Array} array The array to query. + * @returns {*} Returns the first element of `array`. + * @example + * + * _.head([1, 2, 3]); + * // => 1 + * + * _.head([]); + * // => undefined + */ + function head(array) { + return (array && array.length) ? array[0] : undefined; + } + + /** + * Gets the index at which the first occurrence of `value` is found in `array` + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. If `fromIndex` is negative, it's used as the + * offset from the end of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} [fromIndex=0] The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.indexOf([1, 2, 1, 2], 2); + * // => 1 + * + * // Search from the `fromIndex`. + * _.indexOf([1, 2, 1, 2], 2, 2); + * // => 3 + */ + function indexOf(array, value, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = fromIndex == null ? 0 : toInteger(fromIndex); + if (index < 0) { + index = nativeMax(length + index, 0); + } + return baseIndexOf(array, value, index); + } + + /** + * Gets all but the last element of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to query. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.initial([1, 2, 3]); + * // => [1, 2] + */ + function initial(array) { + var length = array == null ? 0 : array.length; + return length ? baseSlice(array, 0, -1) : []; + } + + /** + * Creates an array of unique values that are included in all given arrays + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. The order and references of result values are + * determined by the first array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of intersecting values. + * @example + * + * _.intersection([2, 1], [2, 3]); + * // => [2] + */ + var intersection = baseRest(function(arrays) { + var mapped = arrayMap(arrays, castArrayLikeObject); + return (mapped.length && mapped[0] === arrays[0]) + ? baseIntersection(mapped) + : []; + }); + + /** + * This method is like `_.intersection` except that it accepts `iteratee` + * which is invoked for each element of each `arrays` to generate the criterion + * by which they're compared. The order and references of result values are + * determined by the first array. The iteratee is invoked with one argument: + * (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of intersecting values. + * @example + * + * _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); + * // => [2.1] + * + * // The `_.property` iteratee shorthand. + * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }] + */ + var intersectionBy = baseRest(function(arrays) { + var iteratee = last(arrays), + mapped = arrayMap(arrays, castArrayLikeObject); + + if (iteratee === last(mapped)) { + iteratee = undefined; + } else { + mapped.pop(); + } + return (mapped.length && mapped[0] === arrays[0]) + ? baseIntersection(mapped, getIteratee(iteratee, 2)) + : []; + }); + + /** + * This method is like `_.intersection` except that it accepts `comparator` + * which is invoked to compare elements of `arrays`. The order and references + * of result values are determined by the first array. The comparator is + * invoked with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of intersecting values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.intersectionWith(objects, others, _.isEqual); + * // => [{ 'x': 1, 'y': 2 }] + */ + var intersectionWith = baseRest(function(arrays) { + var comparator = last(arrays), + mapped = arrayMap(arrays, castArrayLikeObject); + + comparator = typeof comparator == 'function' ? comparator : undefined; + if (comparator) { + mapped.pop(); + } + return (mapped.length && mapped[0] === arrays[0]) + ? baseIntersection(mapped, undefined, comparator) + : []; + }); + + /** + * Converts all elements in `array` into a string separated by `separator`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to convert. + * @param {string} [separator=','] The element separator. + * @returns {string} Returns the joined string. + * @example + * + * _.join(['a', 'b', 'c'], '~'); + * // => 'a~b~c' + */ + function join(array, separator) { + return array == null ? '' : nativeJoin.call(array, separator); + } + + /** + * Gets the last element of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to query. + * @returns {*} Returns the last element of `array`. + * @example + * + * _.last([1, 2, 3]); + * // => 3 + */ + function last(array) { + var length = array == null ? 0 : array.length; + return length ? array[length - 1] : undefined; + } + + /** + * This method is like `_.indexOf` except that it iterates over elements of + * `array` from right to left. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} [fromIndex=array.length-1] The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.lastIndexOf([1, 2, 1, 2], 2); + * // => 3 + * + * // Search from the `fromIndex`. + * _.lastIndexOf([1, 2, 1, 2], 2, 2); + * // => 1 + */ + function lastIndexOf(array, value, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = length; + if (fromIndex !== undefined) { + index = toInteger(fromIndex); + index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1); + } + return value === value + ? strictLastIndexOf(array, value, index) + : baseFindIndex(array, baseIsNaN, index, true); + } + + /** + * Gets the element at index `n` of `array`. If `n` is negative, the nth + * element from the end is returned. + * + * @static + * @memberOf _ + * @since 4.11.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=0] The index of the element to return. + * @returns {*} Returns the nth element of `array`. + * @example + * + * var array = ['a', 'b', 'c', 'd']; + * + * _.nth(array, 1); + * // => 'b' + * + * _.nth(array, -2); + * // => 'c'; + */ + function nth(array, n) { + return (array && array.length) ? baseNth(array, toInteger(n)) : undefined; + } + + /** + * Removes all given values from `array` using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove` + * to remove elements from an array by predicate. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {...*} [values] The values to remove. + * @returns {Array} Returns `array`. + * @example + * + * var array = ['a', 'b', 'c', 'a', 'b', 'c']; + * + * _.pull(array, 'a', 'c'); + * console.log(array); + * // => ['b', 'b'] + */ + var pull = baseRest(pullAll); + + /** + * This method is like `_.pull` except that it accepts an array of values to remove. + * + * **Note:** Unlike `_.difference`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @returns {Array} Returns `array`. + * @example + * + * var array = ['a', 'b', 'c', 'a', 'b', 'c']; + * + * _.pullAll(array, ['a', 'c']); + * console.log(array); + * // => ['b', 'b'] + */ + function pullAll(array, values) { + return (array && array.length && values && values.length) + ? basePullAll(array, values) + : array; + } + + /** + * This method is like `_.pullAll` except that it accepts `iteratee` which is + * invoked for each element of `array` and `values` to generate the criterion + * by which they're compared. The iteratee is invoked with one argument: (value). + * + * **Note:** Unlike `_.differenceBy`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns `array`. + * @example + * + * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }]; + * + * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x'); + * console.log(array); + * // => [{ 'x': 2 }] + */ + function pullAllBy(array, values, iteratee) { + return (array && array.length && values && values.length) + ? basePullAll(array, values, getIteratee(iteratee, 2)) + : array; + } + + /** + * This method is like `_.pullAll` except that it accepts `comparator` which + * is invoked to compare elements of `array` to `values`. The comparator is + * invoked with two arguments: (arrVal, othVal). + * + * **Note:** Unlike `_.differenceWith`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 4.6.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns `array`. + * @example + * + * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }]; + * + * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual); + * console.log(array); + * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }] + */ + function pullAllWith(array, values, comparator) { + return (array && array.length && values && values.length) + ? basePullAll(array, values, undefined, comparator) + : array; + } + + /** + * Removes elements from `array` corresponding to `indexes` and returns an + * array of removed elements. + * + * **Note:** Unlike `_.at`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {...(number|number[])} [indexes] The indexes of elements to remove. + * @returns {Array} Returns the new array of removed elements. + * @example + * + * var array = ['a', 'b', 'c', 'd']; + * var pulled = _.pullAt(array, [1, 3]); + * + * console.log(array); + * // => ['a', 'c'] + * + * console.log(pulled); + * // => ['b', 'd'] + */ + var pullAt = flatRest(function(array, indexes) { + var length = array == null ? 0 : array.length, + result = baseAt(array, indexes); + + basePullAt(array, arrayMap(indexes, function(index) { + return isIndex(index, length) ? +index : index; + }).sort(compareAscending)); + + return result; + }); + + /** + * Removes all elements from `array` that `predicate` returns truthy for + * and returns an array of the removed elements. The predicate is invoked + * with three arguments: (value, index, array). + * + * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull` + * to pull elements from an array by value. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new array of removed elements. + * @example + * + * var array = [1, 2, 3, 4]; + * var evens = _.remove(array, function(n) { + * return n % 2 == 0; + * }); + * + * console.log(array); + * // => [1, 3] + * + * console.log(evens); + * // => [2, 4] + */ + function remove(array, predicate) { + var result = []; + if (!(array && array.length)) { + return result; + } + var index = -1, + indexes = [], + length = array.length; + + predicate = getIteratee(predicate, 3); + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result.push(value); + indexes.push(index); + } + } + basePullAt(array, indexes); + return result; + } + + /** + * Reverses `array` so that the first element becomes the last, the second + * element becomes the second to last, and so on. + * + * **Note:** This method mutates `array` and is based on + * [`Array#reverse`](https://mdn.io/Array/reverse). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to modify. + * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3]; + * + * _.reverse(array); + * // => [3, 2, 1] + * + * console.log(array); + * // => [3, 2, 1] + */ + function reverse(array) { + return array == null ? array : nativeReverse.call(array); + } + + /** + * Creates a slice of `array` from `start` up to, but not including, `end`. + * + * **Note:** This method is used instead of + * [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are + * returned. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function slice(array, start, end) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + if (end && typeof end != 'number' && isIterateeCall(array, start, end)) { + start = 0; + end = length; + } + else { + start = start == null ? 0 : toInteger(start); + end = end === undefined ? length : toInteger(end); + } + return baseSlice(array, start, end); + } + + /** + * Uses a binary search to determine the lowest index at which `value` + * should be inserted into `array` in order to maintain its sort order. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedIndex([30, 50], 40); + * // => 1 + */ + function sortedIndex(array, value) { + return baseSortedIndex(array, value); + } + + /** + * This method is like `_.sortedIndex` except that it accepts `iteratee` + * which is invoked for `value` and each element of `array` to compute their + * sort ranking. The iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * var objects = [{ 'x': 4 }, { 'x': 5 }]; + * + * _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); + * // => 0 + * + * // The `_.property` iteratee shorthand. + * _.sortedIndexBy(objects, { 'x': 4 }, 'x'); + * // => 0 + */ + function sortedIndexBy(array, value, iteratee) { + return baseSortedIndexBy(array, value, getIteratee(iteratee, 2)); + } + + /** + * This method is like `_.indexOf` except that it performs a binary + * search on a sorted `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.sortedIndexOf([4, 5, 5, 5, 6], 5); + * // => 1 + */ + function sortedIndexOf(array, value) { + var length = array == null ? 0 : array.length; + if (length) { + var index = baseSortedIndex(array, value); + if (index < length && eq(array[index], value)) { + return index; + } + } + return -1; + } + + /** + * This method is like `_.sortedIndex` except that it returns the highest + * index at which `value` should be inserted into `array` in order to + * maintain its sort order. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedLastIndex([4, 5, 5, 5, 6], 5); + * // => 4 + */ + function sortedLastIndex(array, value) { + return baseSortedIndex(array, value, true); + } + + /** + * This method is like `_.sortedLastIndex` except that it accepts `iteratee` + * which is invoked for `value` and each element of `array` to compute their + * sort ranking. The iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * var objects = [{ 'x': 4 }, { 'x': 5 }]; + * + * _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); + * // => 1 + * + * // The `_.property` iteratee shorthand. + * _.sortedLastIndexBy(objects, { 'x': 4 }, 'x'); + * // => 1 + */ + function sortedLastIndexBy(array, value, iteratee) { + return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true); + } + + /** + * This method is like `_.lastIndexOf` except that it performs a binary + * search on a sorted `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.sortedLastIndexOf([4, 5, 5, 5, 6], 5); + * // => 3 + */ + function sortedLastIndexOf(array, value) { + var length = array == null ? 0 : array.length; + if (length) { + var index = baseSortedIndex(array, value, true) - 1; + if (eq(array[index], value)) { + return index; + } + } + return -1; + } + + /** + * This method is like `_.uniq` except that it's designed and optimized + * for sorted arrays. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.sortedUniq([1, 1, 2]); + * // => [1, 2] + */ + function sortedUniq(array) { + return (array && array.length) + ? baseSortedUniq(array) + : []; + } + + /** + * This method is like `_.uniqBy` except that it's designed and optimized + * for sorted arrays. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor); + * // => [1.1, 2.3] + */ + function sortedUniqBy(array, iteratee) { + return (array && array.length) + ? baseSortedUniq(array, getIteratee(iteratee, 2)) + : []; + } + + /** + * Gets all but the first element of `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to query. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.tail([1, 2, 3]); + * // => [2, 3] + */ + function tail(array) { + var length = array == null ? 0 : array.length; + return length ? baseSlice(array, 1, length) : []; + } + + /** + * Creates a slice of `array` with `n` elements taken from the beginning. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to take. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.take([1, 2, 3]); + * // => [1] + * + * _.take([1, 2, 3], 2); + * // => [1, 2] + * + * _.take([1, 2, 3], 5); + * // => [1, 2, 3] + * + * _.take([1, 2, 3], 0); + * // => [] + */ + function take(array, n, guard) { + if (!(array && array.length)) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + return baseSlice(array, 0, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` with `n` elements taken from the end. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to take. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.takeRight([1, 2, 3]); + * // => [3] + * + * _.takeRight([1, 2, 3], 2); + * // => [2, 3] + * + * _.takeRight([1, 2, 3], 5); + * // => [1, 2, 3] + * + * _.takeRight([1, 2, 3], 0); + * // => [] + */ + function takeRight(array, n, guard) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + n = length - n; + return baseSlice(array, n < 0 ? 0 : n, length); + } + + /** + * Creates a slice of `array` with elements taken from the end. Elements are + * taken until `predicate` returns falsey. The predicate is invoked with + * three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * _.takeRightWhile(users, function(o) { return !o.active; }); + * // => objects for ['fred', 'pebbles'] + * + * // The `_.matches` iteratee shorthand. + * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false }); + * // => objects for ['pebbles'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.takeRightWhile(users, ['active', false]); + * // => objects for ['fred', 'pebbles'] + * + * // The `_.property` iteratee shorthand. + * _.takeRightWhile(users, 'active'); + * // => [] + */ + function takeRightWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3), false, true) + : []; + } + + /** + * Creates a slice of `array` with elements taken from the beginning. Elements + * are taken until `predicate` returns falsey. The predicate is invoked with + * three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * _.takeWhile(users, function(o) { return !o.active; }); + * // => objects for ['barney', 'fred'] + * + * // The `_.matches` iteratee shorthand. + * _.takeWhile(users, { 'user': 'barney', 'active': false }); + * // => objects for ['barney'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.takeWhile(users, ['active', false]); + * // => objects for ['barney', 'fred'] + * + * // The `_.property` iteratee shorthand. + * _.takeWhile(users, 'active'); + * // => [] + */ + function takeWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3)) + : []; + } + + /** + * Creates an array of unique values, in order, from all given arrays using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of combined values. + * @example + * + * _.union([2], [1, 2]); + * // => [2, 1] + */ + var union = baseRest(function(arrays) { + return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true)); + }); + + /** + * This method is like `_.union` except that it accepts `iteratee` which is + * invoked for each element of each `arrays` to generate the criterion by + * which uniqueness is computed. Result values are chosen from the first + * array in which the value occurs. The iteratee is invoked with one argument: + * (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of combined values. + * @example + * + * _.unionBy([2.1], [1.2, 2.3], Math.floor); + * // => [2.1, 1.2] + * + * // The `_.property` iteratee shorthand. + * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }, { 'x': 2 }] + */ + var unionBy = baseRest(function(arrays) { + var iteratee = last(arrays); + if (isArrayLikeObject(iteratee)) { + iteratee = undefined; + } + return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)); + }); + + /** + * This method is like `_.union` except that it accepts `comparator` which + * is invoked to compare elements of `arrays`. Result values are chosen from + * the first array in which the value occurs. The comparator is invoked + * with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of combined values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.unionWith(objects, others, _.isEqual); + * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] + */ + var unionWith = baseRest(function(arrays) { + var comparator = last(arrays); + comparator = typeof comparator == 'function' ? comparator : undefined; + return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator); + }); + + /** + * Creates a duplicate-free version of an array, using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons, in which only the first occurrence of each element + * is kept. The order of result values is determined by the order they occur + * in the array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.uniq([2, 1, 2]); + * // => [2, 1] + */ + function uniq(array) { + return (array && array.length) ? baseUniq(array) : []; + } + + /** + * This method is like `_.uniq` except that it accepts `iteratee` which is + * invoked for each element in `array` to generate the criterion by which + * uniqueness is computed. The order of result values is determined by the + * order they occur in the array. The iteratee is invoked with one argument: + * (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.uniqBy([2.1, 1.2, 2.3], Math.floor); + * // => [2.1, 1.2] + * + * // The `_.property` iteratee shorthand. + * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }, { 'x': 2 }] + */ + function uniqBy(array, iteratee) { + return (array && array.length) ? baseUniq(array, getIteratee(iteratee, 2)) : []; + } + + /** + * This method is like `_.uniq` except that it accepts `comparator` which + * is invoked to compare elements of `array`. The order of result values is + * determined by the order they occur in the array.The comparator is invoked + * with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.uniqWith(objects, _.isEqual); + * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }] + */ + function uniqWith(array, comparator) { + comparator = typeof comparator == 'function' ? comparator : undefined; + return (array && array.length) ? baseUniq(array, undefined, comparator) : []; + } + + /** + * This method is like `_.zip` except that it accepts an array of grouped + * elements and creates an array regrouping the elements to their pre-zip + * configuration. + * + * @static + * @memberOf _ + * @since 1.2.0 + * @category Array + * @param {Array} array The array of grouped elements to process. + * @returns {Array} Returns the new array of regrouped elements. + * @example + * + * var zipped = _.zip(['a', 'b'], [1, 2], [true, false]); + * // => [['a', 1, true], ['b', 2, false]] + * + * _.unzip(zipped); + * // => [['a', 'b'], [1, 2], [true, false]] + */ + function unzip(array) { + if (!(array && array.length)) { + return []; + } + var length = 0; + array = arrayFilter(array, function(group) { + if (isArrayLikeObject(group)) { + length = nativeMax(group.length, length); + return true; + } + }); + return baseTimes(length, function(index) { + return arrayMap(array, baseProperty(index)); + }); + } + + /** + * This method is like `_.unzip` except that it accepts `iteratee` to specify + * how regrouped values should be combined. The iteratee is invoked with the + * elements of each group: (...group). + * + * @static + * @memberOf _ + * @since 3.8.0 + * @category Array + * @param {Array} array The array of grouped elements to process. + * @param {Function} [iteratee=_.identity] The function to combine + * regrouped values. + * @returns {Array} Returns the new array of regrouped elements. + * @example + * + * var zipped = _.zip([1, 2], [10, 20], [100, 200]); + * // => [[1, 10, 100], [2, 20, 200]] + * + * _.unzipWith(zipped, _.add); + * // => [3, 30, 300] + */ + function unzipWith(array, iteratee) { + if (!(array && array.length)) { + return []; + } + var result = unzip(array); + if (iteratee == null) { + return result; + } + return arrayMap(result, function(group) { + return apply(iteratee, undefined, group); + }); + } + + /** + * Creates an array excluding all given values using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * **Note:** Unlike `_.pull`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...*} [values] The values to exclude. + * @returns {Array} Returns the new array of filtered values. + * @see _.difference, _.xor + * @example + * + * _.without([2, 1, 2, 3], 1, 2); + * // => [3] + */ + var without = baseRest(function(array, values) { + return isArrayLikeObject(array) + ? baseDifference(array, values) + : []; + }); + + /** + * Creates an array of unique values that is the + * [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference) + * of the given arrays. The order of result values is determined by the order + * they occur in the arrays. + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of filtered values. + * @see _.difference, _.without + * @example + * + * _.xor([2, 1], [2, 3]); + * // => [1, 3] + */ + var xor = baseRest(function(arrays) { + return baseXor(arrayFilter(arrays, isArrayLikeObject)); + }); + + /** + * This method is like `_.xor` except that it accepts `iteratee` which is + * invoked for each element of each `arrays` to generate the criterion by + * which by which they're compared. The order of result values is determined + * by the order they occur in the arrays. The iteratee is invoked with one + * argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor); + * // => [1.2, 3.4] + * + * // The `_.property` iteratee shorthand. + * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 2 }] + */ + var xorBy = baseRest(function(arrays) { + var iteratee = last(arrays); + if (isArrayLikeObject(iteratee)) { + iteratee = undefined; + } + return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2)); + }); + + /** + * This method is like `_.xor` except that it accepts `comparator` which is + * invoked to compare elements of `arrays`. The order of result values is + * determined by the order they occur in the arrays. The comparator is invoked + * with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.xorWith(objects, others, _.isEqual); + * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] + */ + var xorWith = baseRest(function(arrays) { + var comparator = last(arrays); + comparator = typeof comparator == 'function' ? comparator : undefined; + return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator); + }); + + /** + * Creates an array of grouped elements, the first of which contains the + * first elements of the given arrays, the second of which contains the + * second elements of the given arrays, and so on. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {...Array} [arrays] The arrays to process. + * @returns {Array} Returns the new array of grouped elements. + * @example + * + * _.zip(['a', 'b'], [1, 2], [true, false]); + * // => [['a', 1, true], ['b', 2, false]] + */ + var zip = baseRest(unzip); + + /** + * This method is like `_.fromPairs` except that it accepts two arrays, + * one of property identifiers and one of corresponding values. + * + * @static + * @memberOf _ + * @since 0.4.0 + * @category Array + * @param {Array} [props=[]] The property identifiers. + * @param {Array} [values=[]] The property values. + * @returns {Object} Returns the new object. + * @example + * + * _.zipObject(['a', 'b'], [1, 2]); + * // => { 'a': 1, 'b': 2 } + */ + function zipObject(props, values) { + return baseZipObject(props || [], values || [], assignValue); + } + + /** + * This method is like `_.zipObject` except that it supports property paths. + * + * @static + * @memberOf _ + * @since 4.1.0 + * @category Array + * @param {Array} [props=[]] The property identifiers. + * @param {Array} [values=[]] The property values. + * @returns {Object} Returns the new object. + * @example + * + * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]); + * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } } + */ + function zipObjectDeep(props, values) { + return baseZipObject(props || [], values || [], baseSet); + } + + /** + * This method is like `_.zip` except that it accepts `iteratee` to specify + * how grouped values should be combined. The iteratee is invoked with the + * elements of each group: (...group). + * + * @static + * @memberOf _ + * @since 3.8.0 + * @category Array + * @param {...Array} [arrays] The arrays to process. + * @param {Function} [iteratee=_.identity] The function to combine + * grouped values. + * @returns {Array} Returns the new array of grouped elements. + * @example + * + * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) { + * return a + b + c; + * }); + * // => [111, 222] + */ + var zipWith = baseRest(function(arrays) { + var length = arrays.length, + iteratee = length > 1 ? arrays[length - 1] : undefined; + + iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined; + return unzipWith(arrays, iteratee); + }); + + /*------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` wrapper instance that wraps `value` with explicit method + * chain sequences enabled. The result of such sequences must be unwrapped + * with `_#value`. + * + * @static + * @memberOf _ + * @since 1.3.0 + * @category Seq + * @param {*} value The value to wrap. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'pebbles', 'age': 1 } + * ]; + * + * var youngest = _ + * .chain(users) + * .sortBy('age') + * .map(function(o) { + * return o.user + ' is ' + o.age; + * }) + * .head() + * .value(); + * // => 'pebbles is 1' + */ + function chain(value) { + var result = lodash(value); + result.__chain__ = true; + return result; + } + + /** + * This method invokes `interceptor` and returns `value`. The interceptor + * is invoked with one argument; (value). The purpose of this method is to + * "tap into" a method chain sequence in order to modify intermediate results. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Seq + * @param {*} value The value to provide to `interceptor`. + * @param {Function} interceptor The function to invoke. + * @returns {*} Returns `value`. + * @example + * + * _([1, 2, 3]) + * .tap(function(array) { + * // Mutate input array. + * array.pop(); + * }) + * .reverse() + * .value(); + * // => [2, 1] + */ + function tap(value, interceptor) { + interceptor(value); + return value; + } + + /** + * This method is like `_.tap` except that it returns the result of `interceptor`. + * The purpose of this method is to "pass thru" values replacing intermediate + * results in a method chain sequence. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Seq + * @param {*} value The value to provide to `interceptor`. + * @param {Function} interceptor The function to invoke. + * @returns {*} Returns the result of `interceptor`. + * @example + * + * _(' abc ') + * .chain() + * .trim() + * .thru(function(value) { + * return [value]; + * }) + * .value(); + * // => ['abc'] + */ + function thru(value, interceptor) { + return interceptor(value); + } + + /** + * This method is the wrapper version of `_.at`. + * + * @name at + * @memberOf _ + * @since 1.0.0 + * @category Seq + * @param {...(string|string[])} [paths] The property paths to pick. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; + * + * _(object).at(['a[0].b.c', 'a[1]']).value(); + * // => [3, 4] + */ + var wrapperAt = flatRest(function(paths) { + var length = paths.length, + start = length ? paths[0] : 0, + value = this.__wrapped__, + interceptor = function(object) { return baseAt(object, paths); }; + + if (length > 1 || this.__actions__.length || + !(value instanceof LazyWrapper) || !isIndex(start)) { + return this.thru(interceptor); + } + value = value.slice(start, +start + (length ? 1 : 0)); + value.__actions__.push({ + 'func': thru, + 'args': [interceptor], + 'thisArg': undefined + }); + return new LodashWrapper(value, this.__chain__).thru(function(array) { + if (length && !array.length) { + array.push(undefined); + } + return array; + }); + }); + + /** + * Creates a `lodash` wrapper instance with explicit method chain sequences enabled. + * + * @name chain + * @memberOf _ + * @since 0.1.0 + * @category Seq + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 } + * ]; + * + * // A sequence without explicit chaining. + * _(users).head(); + * // => { 'user': 'barney', 'age': 36 } + * + * // A sequence with explicit chaining. + * _(users) + * .chain() + * .head() + * .pick('user') + * .value(); + * // => { 'user': 'barney' } + */ + function wrapperChain() { + return chain(this); + } + + /** + * Executes the chain sequence and returns the wrapped result. + * + * @name commit + * @memberOf _ + * @since 3.2.0 + * @category Seq + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var array = [1, 2]; + * var wrapped = _(array).push(3); + * + * console.log(array); + * // => [1, 2] + * + * wrapped = wrapped.commit(); + * console.log(array); + * // => [1, 2, 3] + * + * wrapped.last(); + * // => 3 + * + * console.log(array); + * // => [1, 2, 3] + */ + function wrapperCommit() { + return new LodashWrapper(this.value(), this.__chain__); + } + + /** + * Gets the next value on a wrapped object following the + * [iterator protocol](https://mdn.io/iteration_protocols#iterator). + * + * @name next + * @memberOf _ + * @since 4.0.0 + * @category Seq + * @returns {Object} Returns the next iterator value. + * @example + * + * var wrapped = _([1, 2]); + * + * wrapped.next(); + * // => { 'done': false, 'value': 1 } + * + * wrapped.next(); + * // => { 'done': false, 'value': 2 } + * + * wrapped.next(); + * // => { 'done': true, 'value': undefined } + */ + function wrapperNext() { + if (this.__values__ === undefined) { + this.__values__ = toArray(this.value()); + } + var done = this.__index__ >= this.__values__.length, + value = done ? undefined : this.__values__[this.__index__++]; + + return { 'done': done, 'value': value }; + } + + /** + * Enables the wrapper to be iterable. + * + * @name Symbol.iterator + * @memberOf _ + * @since 4.0.0 + * @category Seq + * @returns {Object} Returns the wrapper object. + * @example + * + * var wrapped = _([1, 2]); + * + * wrapped[Symbol.iterator]() === wrapped; + * // => true + * + * Array.from(wrapped); + * // => [1, 2] + */ + function wrapperToIterator() { + return this; + } + + /** + * Creates a clone of the chain sequence planting `value` as the wrapped value. + * + * @name plant + * @memberOf _ + * @since 3.2.0 + * @category Seq + * @param {*} value The value to plant. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * function square(n) { + * return n * n; + * } + * + * var wrapped = _([1, 2]).map(square); + * var other = wrapped.plant([3, 4]); + * + * other.value(); + * // => [9, 16] + * + * wrapped.value(); + * // => [1, 4] + */ + function wrapperPlant(value) { + var result, + parent = this; + + while (parent instanceof baseLodash) { + var clone = wrapperClone(parent); + clone.__index__ = 0; + clone.__values__ = undefined; + if (result) { + previous.__wrapped__ = clone; + } else { + result = clone; + } + var previous = clone; + parent = parent.__wrapped__; + } + previous.__wrapped__ = value; + return result; + } + + /** + * This method is the wrapper version of `_.reverse`. + * + * **Note:** This method mutates the wrapped array. + * + * @name reverse + * @memberOf _ + * @since 0.1.0 + * @category Seq + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var array = [1, 2, 3]; + * + * _(array).reverse().value() + * // => [3, 2, 1] + * + * console.log(array); + * // => [3, 2, 1] + */ + function wrapperReverse() { + var value = this.__wrapped__; + if (value instanceof LazyWrapper) { + var wrapped = value; + if (this.__actions__.length) { + wrapped = new LazyWrapper(this); + } + wrapped = wrapped.reverse(); + wrapped.__actions__.push({ + 'func': thru, + 'args': [reverse], + 'thisArg': undefined + }); + return new LodashWrapper(wrapped, this.__chain__); + } + return this.thru(reverse); + } + + /** + * Executes the chain sequence to resolve the unwrapped value. + * + * @name value + * @memberOf _ + * @since 0.1.0 + * @alias toJSON, valueOf + * @category Seq + * @returns {*} Returns the resolved unwrapped value. + * @example + * + * _([1, 2, 3]).value(); + * // => [1, 2, 3] + */ + function wrapperValue() { + return baseWrapperValue(this.__wrapped__, this.__actions__); + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` thru `iteratee`. The corresponding value of + * each key is the number of times the key was returned by `iteratee`. The + * iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 0.5.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The iteratee to transform keys. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.countBy([6.1, 4.2, 6.3], Math.floor); + * // => { '4': 1, '6': 2 } + * + * // The `_.property` iteratee shorthand. + * _.countBy(['one', 'two', 'three'], 'length'); + * // => { '3': 2, '5': 1 } + */ + var countBy = createAggregator(function(result, value, key) { + if (hasOwnProperty.call(result, key)) { + ++result[key]; + } else { + baseAssignValue(result, key, 1); + } + }); + + /** + * Checks if `predicate` returns truthy for **all** elements of `collection`. + * Iteration is stopped once `predicate` returns falsey. The predicate is + * invoked with three arguments: (value, index|key, collection). + * + * **Note:** This method returns `true` for + * [empty collections](https://en.wikipedia.org/wiki/Empty_set) because + * [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of + * elements of empty collections. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false`. + * @example + * + * _.every([true, 1, null, 'yes'], Boolean); + * // => false + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': false } + * ]; + * + * // The `_.matches` iteratee shorthand. + * _.every(users, { 'user': 'barney', 'active': false }); + * // => false + * + * // The `_.matchesProperty` iteratee shorthand. + * _.every(users, ['active', false]); + * // => true + * + * // The `_.property` iteratee shorthand. + * _.every(users, 'active'); + * // => false + */ + function every(collection, predicate, guard) { + var func = isArray(collection) ? arrayEvery : baseEvery; + if (guard && isIterateeCall(collection, predicate, guard)) { + predicate = undefined; + } + return func(collection, getIteratee(predicate, 3)); + } + + /** + * Iterates over elements of `collection`, returning an array of all elements + * `predicate` returns truthy for. The predicate is invoked with three + * arguments: (value, index|key, collection). + * + * **Note:** Unlike `_.remove`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + * @see _.reject + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false } + * ]; + * + * _.filter(users, function(o) { return !o.active; }); + * // => objects for ['fred'] + * + * // The `_.matches` iteratee shorthand. + * _.filter(users, { 'age': 36, 'active': true }); + * // => objects for ['barney'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.filter(users, ['active', false]); + * // => objects for ['fred'] + * + * // The `_.property` iteratee shorthand. + * _.filter(users, 'active'); + * // => objects for ['barney'] + */ + function filter(collection, predicate) { + var func = isArray(collection) ? arrayFilter : baseFilter; + return func(collection, getIteratee(predicate, 3)); + } + + /** + * Iterates over elements of `collection`, returning the first element + * `predicate` returns truthy for. The predicate is invoked with three + * arguments: (value, index|key, collection). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=0] The index to search from. + * @returns {*} Returns the matched element, else `undefined`. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false }, + * { 'user': 'pebbles', 'age': 1, 'active': true } + * ]; + * + * _.find(users, function(o) { return o.age < 40; }); + * // => object for 'barney' + * + * // The `_.matches` iteratee shorthand. + * _.find(users, { 'age': 1, 'active': true }); + * // => object for 'pebbles' + * + * // The `_.matchesProperty` iteratee shorthand. + * _.find(users, ['active', false]); + * // => object for 'fred' + * + * // The `_.property` iteratee shorthand. + * _.find(users, 'active'); + * // => object for 'barney' + */ + var find = createFind(findIndex); + + /** + * This method is like `_.find` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Collection + * @param {Array|Object} collection The collection to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=collection.length-1] The index to search from. + * @returns {*} Returns the matched element, else `undefined`. + * @example + * + * _.findLast([1, 2, 3, 4], function(n) { + * return n % 2 == 1; + * }); + * // => 3 + */ + var findLast = createFind(findLastIndex); + + /** + * Creates a flattened array of values by running each element in `collection` + * thru `iteratee` and flattening the mapped results. The iteratee is invoked + * with three arguments: (value, index|key, collection). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [n, n]; + * } + * + * _.flatMap([1, 2], duplicate); + * // => [1, 1, 2, 2] + */ + function flatMap(collection, iteratee) { + return baseFlatten(map(collection, iteratee), 1); + } + + /** + * This method is like `_.flatMap` except that it recursively flattens the + * mapped results. + * + * @static + * @memberOf _ + * @since 4.7.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [[[n, n]]]; + * } + * + * _.flatMapDeep([1, 2], duplicate); + * // => [1, 1, 2, 2] + */ + function flatMapDeep(collection, iteratee) { + return baseFlatten(map(collection, iteratee), INFINITY); + } + + /** + * This method is like `_.flatMap` except that it recursively flattens the + * mapped results up to `depth` times. + * + * @static + * @memberOf _ + * @since 4.7.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {number} [depth=1] The maximum recursion depth. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [[[n, n]]]; + * } + * + * _.flatMapDepth([1, 2], duplicate, 2); + * // => [[1, 1], [2, 2]] + */ + function flatMapDepth(collection, iteratee, depth) { + depth = depth === undefined ? 1 : toInteger(depth); + return baseFlatten(map(collection, iteratee), depth); + } + + /** + * Iterates over elements of `collection` and invokes `iteratee` for each element. + * The iteratee is invoked with three arguments: (value, index|key, collection). + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * **Note:** As with other "Collections" methods, objects with a "length" + * property are iterated like arrays. To avoid this behavior use `_.forIn` + * or `_.forOwn` for object iteration. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @alias each + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + * @see _.forEachRight + * @example + * + * _.forEach([1, 2], function(value) { + * console.log(value); + * }); + * // => Logs `1` then `2`. + * + * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a' then 'b' (iteration order is not guaranteed). + */ + function forEach(collection, iteratee) { + var func = isArray(collection) ? arrayEach : baseEach; + return func(collection, getIteratee(iteratee, 3)); + } + + /** + * This method is like `_.forEach` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @alias eachRight + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + * @see _.forEach + * @example + * + * _.forEachRight([1, 2], function(value) { + * console.log(value); + * }); + * // => Logs `2` then `1`. + */ + function forEachRight(collection, iteratee) { + var func = isArray(collection) ? arrayEachRight : baseEachRight; + return func(collection, getIteratee(iteratee, 3)); + } + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` thru `iteratee`. The order of grouped values + * is determined by the order they occur in `collection`. The corresponding + * value of each key is an array of elements responsible for generating the + * key. The iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The iteratee to transform keys. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.groupBy([6.1, 4.2, 6.3], Math.floor); + * // => { '4': [4.2], '6': [6.1, 6.3] } + * + * // The `_.property` iteratee shorthand. + * _.groupBy(['one', 'two', 'three'], 'length'); + * // => { '3': ['one', 'two'], '5': ['three'] } + */ + var groupBy = createAggregator(function(result, value, key) { + if (hasOwnProperty.call(result, key)) { + result[key].push(value); + } else { + baseAssignValue(result, key, [value]); + } + }); + + /** + * Checks if `value` is in `collection`. If `collection` is a string, it's + * checked for a substring of `value`, otherwise + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * is used for equality comparisons. If `fromIndex` is negative, it's used as + * the offset from the end of `collection`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object|string} collection The collection to inspect. + * @param {*} value The value to search for. + * @param {number} [fromIndex=0] The index to search from. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. + * @returns {boolean} Returns `true` if `value` is found, else `false`. + * @example + * + * _.includes([1, 2, 3], 1); + * // => true + * + * _.includes([1, 2, 3], 1, 2); + * // => false + * + * _.includes({ 'a': 1, 'b': 2 }, 1); + * // => true + * + * _.includes('abcd', 'bc'); + * // => true + */ + function includes(collection, value, fromIndex, guard) { + collection = isArrayLike(collection) ? collection : values(collection); + fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0; + + var length = collection.length; + if (fromIndex < 0) { + fromIndex = nativeMax(length + fromIndex, 0); + } + return isString(collection) + ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1) + : (!!length && baseIndexOf(collection, value, fromIndex) > -1); + } + + /** + * Invokes the method at `path` of each element in `collection`, returning + * an array of the results of each invoked method. Any additional arguments + * are provided to each invoked method. If `path` is a function, it's invoked + * for, and `this` bound to, each element in `collection`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Array|Function|string} path The path of the method to invoke or + * the function invoked per iteration. + * @param {...*} [args] The arguments to invoke each method with. + * @returns {Array} Returns the array of results. + * @example + * + * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort'); + * // => [[1, 5, 7], [1, 2, 3]] + * + * _.invokeMap([123, 456], String.prototype.split, ''); + * // => [['1', '2', '3'], ['4', '5', '6']] + */ + var invokeMap = baseRest(function(collection, path, args) { + var index = -1, + isFunc = typeof path == 'function', + result = isArrayLike(collection) ? Array(collection.length) : []; + + baseEach(collection, function(value) { + result[++index] = isFunc ? apply(path, value, args) : baseInvoke(value, path, args); + }); + return result; + }); + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` thru `iteratee`. The corresponding value of + * each key is the last element responsible for generating the key. The + * iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The iteratee to transform keys. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * var array = [ + * { 'dir': 'left', 'code': 97 }, + * { 'dir': 'right', 'code': 100 } + * ]; + * + * _.keyBy(array, function(o) { + * return String.fromCharCode(o.code); + * }); + * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } + * + * _.keyBy(array, 'dir'); + * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } + */ + var keyBy = createAggregator(function(result, value, key) { + baseAssignValue(result, key, value); + }); + + /** + * Creates an array of values by running each element in `collection` thru + * `iteratee`. The iteratee is invoked with three arguments: + * (value, index|key, collection). + * + * Many lodash methods are guarded to work as iteratees for methods like + * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`. + * + * The guarded methods are: + * `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`, + * `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`, + * `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`, + * `template`, `trim`, `trimEnd`, `trimStart`, and `words` + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + * @example + * + * function square(n) { + * return n * n; + * } + * + * _.map([4, 8], square); + * // => [16, 64] + * + * _.map({ 'a': 4, 'b': 8 }, square); + * // => [16, 64] (iteration order is not guaranteed) + * + * var users = [ + * { 'user': 'barney' }, + * { 'user': 'fred' } + * ]; + * + * // The `_.property` iteratee shorthand. + * _.map(users, 'user'); + * // => ['barney', 'fred'] + */ + function map(collection, iteratee) { + var func = isArray(collection) ? arrayMap : baseMap; + return func(collection, getIteratee(iteratee, 3)); + } + + /** + * This method is like `_.sortBy` except that it allows specifying the sort + * orders of the iteratees to sort by. If `orders` is unspecified, all values + * are sorted in ascending order. Otherwise, specify an order of "desc" for + * descending or "asc" for ascending sort order of corresponding values. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]] + * The iteratees to sort by. + * @param {string[]} [orders] The sort orders of `iteratees`. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. + * @returns {Array} Returns the new sorted array. + * @example + * + * var users = [ + * { 'user': 'fred', 'age': 48 }, + * { 'user': 'barney', 'age': 34 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'barney', 'age': 36 } + * ]; + * + * // Sort by `user` in ascending order and by `age` in descending order. + * _.orderBy(users, ['user', 'age'], ['asc', 'desc']); + * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] + */ + function orderBy(collection, iteratees, orders, guard) { + if (collection == null) { + return []; + } + if (!isArray(iteratees)) { + iteratees = iteratees == null ? [] : [iteratees]; + } + orders = guard ? undefined : orders; + if (!isArray(orders)) { + orders = orders == null ? [] : [orders]; + } + return baseOrderBy(collection, iteratees, orders); + } + + /** + * Creates an array of elements split into two groups, the first of which + * contains elements `predicate` returns truthy for, the second of which + * contains elements `predicate` returns falsey for. The predicate is + * invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the array of grouped elements. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': true }, + * { 'user': 'pebbles', 'age': 1, 'active': false } + * ]; + * + * _.partition(users, function(o) { return o.active; }); + * // => objects for [['fred'], ['barney', 'pebbles']] + * + * // The `_.matches` iteratee shorthand. + * _.partition(users, { 'age': 1, 'active': false }); + * // => objects for [['pebbles'], ['barney', 'fred']] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.partition(users, ['active', false]); + * // => objects for [['barney', 'pebbles'], ['fred']] + * + * // The `_.property` iteratee shorthand. + * _.partition(users, 'active'); + * // => objects for [['fred'], ['barney', 'pebbles']] + */ + var partition = createAggregator(function(result, value, key) { + result[key ? 0 : 1].push(value); + }, function() { return [[], []]; }); + + /** + * Reduces `collection` to a value which is the accumulated result of running + * each element in `collection` thru `iteratee`, where each successive + * invocation is supplied the return value of the previous. If `accumulator` + * is not given, the first element of `collection` is used as the initial + * value. The iteratee is invoked with four arguments: + * (accumulator, value, index|key, collection). + * + * Many lodash methods are guarded to work as iteratees for methods like + * `_.reduce`, `_.reduceRight`, and `_.transform`. + * + * The guarded methods are: + * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`, + * and `sortBy` + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @returns {*} Returns the accumulated value. + * @see _.reduceRight + * @example + * + * _.reduce([1, 2], function(sum, n) { + * return sum + n; + * }, 0); + * // => 3 + * + * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { + * (result[value] || (result[value] = [])).push(key); + * return result; + * }, {}); + * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed) + */ + function reduce(collection, iteratee, accumulator) { + var func = isArray(collection) ? arrayReduce : baseReduce, + initAccum = arguments.length < 3; + + return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach); + } + + /** + * This method is like `_.reduce` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @returns {*} Returns the accumulated value. + * @see _.reduce + * @example + * + * var array = [[0, 1], [2, 3], [4, 5]]; + * + * _.reduceRight(array, function(flattened, other) { + * return flattened.concat(other); + * }, []); + * // => [4, 5, 2, 3, 0, 1] + */ + function reduceRight(collection, iteratee, accumulator) { + var func = isArray(collection) ? arrayReduceRight : baseReduce, + initAccum = arguments.length < 3; + + return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight); + } + + /** + * The opposite of `_.filter`; this method returns the elements of `collection` + * that `predicate` does **not** return truthy for. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + * @see _.filter + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': true } + * ]; + * + * _.reject(users, function(o) { return !o.active; }); + * // => objects for ['fred'] + * + * // The `_.matches` iteratee shorthand. + * _.reject(users, { 'age': 40, 'active': true }); + * // => objects for ['barney'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.reject(users, ['active', false]); + * // => objects for ['fred'] + * + * // The `_.property` iteratee shorthand. + * _.reject(users, 'active'); + * // => objects for ['barney'] + */ + function reject(collection, predicate) { + var func = isArray(collection) ? arrayFilter : baseFilter; + return func(collection, negate(getIteratee(predicate, 3))); + } + + /** + * Gets a random element from `collection`. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Collection + * @param {Array|Object} collection The collection to sample. + * @returns {*} Returns the random element. + * @example + * + * _.sample([1, 2, 3, 4]); + * // => 2 + */ + function sample(collection) { + var func = isArray(collection) ? arraySample : baseSample; + return func(collection); + } + + /** + * Gets `n` random elements at unique keys from `collection` up to the + * size of `collection`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to sample. + * @param {number} [n=1] The number of elements to sample. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the random elements. + * @example + * + * _.sampleSize([1, 2, 3], 2); + * // => [3, 1] + * + * _.sampleSize([1, 2, 3], 4); + * // => [2, 3, 1] + */ + function sampleSize(collection, n, guard) { + if ((guard ? isIterateeCall(collection, n, guard) : n === undefined)) { + n = 1; + } else { + n = toInteger(n); + } + var func = isArray(collection) ? arraySampleSize : baseSampleSize; + return func(collection, n); + } + + /** + * Creates an array of shuffled values, using a version of the + * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to shuffle. + * @returns {Array} Returns the new shuffled array. + * @example + * + * _.shuffle([1, 2, 3, 4]); + * // => [4, 1, 3, 2] + */ + function shuffle(collection) { + var func = isArray(collection) ? arrayShuffle : baseShuffle; + return func(collection); + } + + /** + * Gets the size of `collection` by returning its length for array-like + * values or the number of own enumerable string keyed properties for objects. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object|string} collection The collection to inspect. + * @returns {number} Returns the collection size. + * @example + * + * _.size([1, 2, 3]); + * // => 3 + * + * _.size({ 'a': 1, 'b': 2 }); + * // => 2 + * + * _.size('pebbles'); + * // => 7 + */ + function size(collection) { + if (collection == null) { + return 0; + } + if (isArrayLike(collection)) { + return isString(collection) ? stringSize(collection) : collection.length; + } + var tag = getTag(collection); + if (tag == mapTag || tag == setTag) { + return collection.size; + } + return baseKeys(collection).length; + } + + /** + * Checks if `predicate` returns truthy for **any** element of `collection`. + * Iteration is stopped once `predicate` returns truthy. The predicate is + * invoked with three arguments: (value, index|key, collection). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + * @example + * + * _.some([null, 0, 'yes', false], Boolean); + * // => true + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false } + * ]; + * + * // The `_.matches` iteratee shorthand. + * _.some(users, { 'user': 'barney', 'active': false }); + * // => false + * + * // The `_.matchesProperty` iteratee shorthand. + * _.some(users, ['active', false]); + * // => true + * + * // The `_.property` iteratee shorthand. + * _.some(users, 'active'); + * // => true + */ + function some(collection, predicate, guard) { + var func = isArray(collection) ? arraySome : baseSome; + if (guard && isIterateeCall(collection, predicate, guard)) { + predicate = undefined; + } + return func(collection, getIteratee(predicate, 3)); + } + + /** + * Creates an array of elements, sorted in ascending order by the results of + * running each element in a collection thru each iteratee. This method + * performs a stable sort, that is, it preserves the original sort order of + * equal elements. The iteratees are invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {...(Function|Function[])} [iteratees=[_.identity]] + * The iteratees to sort by. + * @returns {Array} Returns the new sorted array. + * @example + * + * var users = [ + * { 'user': 'fred', 'age': 48 }, + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'barney', 'age': 34 } + * ]; + * + * _.sortBy(users, [function(o) { return o.user; }]); + * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] + * + * _.sortBy(users, ['user', 'age']); + * // => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]] + */ + var sortBy = baseRest(function(collection, iteratees) { + if (collection == null) { + return []; + } + var length = iteratees.length; + if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) { + iteratees = []; + } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) { + iteratees = [iteratees[0]]; + } + return baseOrderBy(collection, baseFlatten(iteratees, 1), []); + }); + + /*------------------------------------------------------------------------*/ + + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now = ctxNow || function() { + return root.Date.now(); + }; + + /*------------------------------------------------------------------------*/ + + /** + * The opposite of `_.before`; this method creates a function that invokes + * `func` once it's called `n` or more times. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {number} n The number of calls before `func` is invoked. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var saves = ['profile', 'settings']; + * + * var done = _.after(saves.length, function() { + * console.log('done saving!'); + * }); + * + * _.forEach(saves, function(type) { + * asyncSave({ 'type': type, 'complete': done }); + * }); + * // => Logs 'done saving!' after the two async saves have completed. + */ + function after(n, func) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + n = toInteger(n); + return function() { + if (--n < 1) { + return func.apply(this, arguments); + } + }; + } + + /** + * Creates a function that invokes `func`, with up to `n` arguments, + * ignoring any additional arguments. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} func The function to cap arguments for. + * @param {number} [n=func.length] The arity cap. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the new capped function. + * @example + * + * _.map(['6', '8', '10'], _.ary(parseInt, 1)); + * // => [6, 8, 10] + */ + function ary(func, n, guard) { + n = guard ? undefined : n; + n = (func && n == null) ? func.length : n; + return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n); + } + + /** + * Creates a function that invokes `func`, with the `this` binding and arguments + * of the created function, while it's called less than `n` times. Subsequent + * calls to the created function return the result of the last `func` invocation. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {number} n The number of calls at which `func` is no longer invoked. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * jQuery(element).on('click', _.before(5, addContactToList)); + * // => Allows adding up to 4 contacts to the list. + */ + function before(n, func) { + var result; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + n = toInteger(n); + return function() { + if (--n > 0) { + result = func.apply(this, arguments); + } + if (n <= 1) { + func = undefined; + } + return result; + }; + } + + /** + * Creates a function that invokes `func` with the `this` binding of `thisArg` + * and `partials` prepended to the arguments it receives. + * + * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds, + * may be used as a placeholder for partially applied arguments. + * + * **Note:** Unlike native `Function#bind`, this method doesn't set the "length" + * property of bound functions. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to bind. + * @param {*} thisArg The `this` binding of `func`. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * function greet(greeting, punctuation) { + * return greeting + ' ' + this.user + punctuation; + * } + * + * var object = { 'user': 'fred' }; + * + * var bound = _.bind(greet, object, 'hi'); + * bound('!'); + * // => 'hi fred!' + * + * // Bound with placeholders. + * var bound = _.bind(greet, object, _, '!'); + * bound('hi'); + * // => 'hi fred!' + */ + var bind = baseRest(function(func, thisArg, partials) { + var bitmask = WRAP_BIND_FLAG; + if (partials.length) { + var holders = replaceHolders(partials, getHolder(bind)); + bitmask |= WRAP_PARTIAL_FLAG; + } + return createWrap(func, bitmask, thisArg, partials, holders); + }); + + /** + * Creates a function that invokes the method at `object[key]` with `partials` + * prepended to the arguments it receives. + * + * This method differs from `_.bind` by allowing bound functions to reference + * methods that may be redefined or don't yet exist. See + * [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern) + * for more details. + * + * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * @static + * @memberOf _ + * @since 0.10.0 + * @category Function + * @param {Object} object The object to invoke the method on. + * @param {string} key The key of the method. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var object = { + * 'user': 'fred', + * 'greet': function(greeting, punctuation) { + * return greeting + ' ' + this.user + punctuation; + * } + * }; + * + * var bound = _.bindKey(object, 'greet', 'hi'); + * bound('!'); + * // => 'hi fred!' + * + * object.greet = function(greeting, punctuation) { + * return greeting + 'ya ' + this.user + punctuation; + * }; + * + * bound('!'); + * // => 'hiya fred!' + * + * // Bound with placeholders. + * var bound = _.bindKey(object, 'greet', _, '!'); + * bound('hi'); + * // => 'hiya fred!' + */ + var bindKey = baseRest(function(object, key, partials) { + var bitmask = WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG; + if (partials.length) { + var holders = replaceHolders(partials, getHolder(bindKey)); + bitmask |= WRAP_PARTIAL_FLAG; + } + return createWrap(key, bitmask, object, partials, holders); + }); + + /** + * Creates a function that accepts arguments of `func` and either invokes + * `func` returning its result, if at least `arity` number of arguments have + * been provided, or returns a function that accepts the remaining `func` + * arguments, and so on. The arity of `func` may be specified if `func.length` + * is not sufficient. + * + * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds, + * may be used as a placeholder for provided arguments. + * + * **Note:** This method doesn't set the "length" property of curried functions. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Function + * @param {Function} func The function to curry. + * @param {number} [arity=func.length] The arity of `func`. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the new curried function. + * @example + * + * var abc = function(a, b, c) { + * return [a, b, c]; + * }; + * + * var curried = _.curry(abc); + * + * curried(1)(2)(3); + * // => [1, 2, 3] + * + * curried(1, 2)(3); + * // => [1, 2, 3] + * + * curried(1, 2, 3); + * // => [1, 2, 3] + * + * // Curried with placeholders. + * curried(1)(_, 3)(2); + * // => [1, 2, 3] + */ + function curry(func, arity, guard) { + arity = guard ? undefined : arity; + var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity); + result.placeholder = curry.placeholder; + return result; + } + + /** + * This method is like `_.curry` except that arguments are applied to `func` + * in the manner of `_.partialRight` instead of `_.partial`. + * + * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for provided arguments. + * + * **Note:** This method doesn't set the "length" property of curried functions. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} func The function to curry. + * @param {number} [arity=func.length] The arity of `func`. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the new curried function. + * @example + * + * var abc = function(a, b, c) { + * return [a, b, c]; + * }; + * + * var curried = _.curryRight(abc); + * + * curried(3)(2)(1); + * // => [1, 2, 3] + * + * curried(2, 3)(1); + * // => [1, 2, 3] + * + * curried(1, 2, 3); + * // => [1, 2, 3] + * + * // Curried with placeholders. + * curried(3)(1, _)(2); + * // => [1, 2, 3] + */ + function curryRight(func, arity, guard) { + arity = guard ? undefined : arity; + var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity); + result.placeholder = curryRight.placeholder; + return result; + } + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + timeWaiting = wait - timeSinceLastCall; + + return maxing + ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) + : timeWaiting; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + + /** + * Defers invoking the `func` until the current call stack has cleared. Any + * additional arguments are provided to `func` when it's invoked. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to defer. + * @param {...*} [args] The arguments to invoke `func` with. + * @returns {number} Returns the timer id. + * @example + * + * _.defer(function(text) { + * console.log(text); + * }, 'deferred'); + * // => Logs 'deferred' after one millisecond. + */ + var defer = baseRest(function(func, args) { + return baseDelay(func, 1, args); + }); + + /** + * Invokes `func` after `wait` milliseconds. Any additional arguments are + * provided to `func` when it's invoked. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @param {...*} [args] The arguments to invoke `func` with. + * @returns {number} Returns the timer id. + * @example + * + * _.delay(function(text) { + * console.log(text); + * }, 1000, 'later'); + * // => Logs 'later' after one second. + */ + var delay = baseRest(function(func, wait, args) { + return baseDelay(func, toNumber(wait) || 0, args); + }); + + /** + * Creates a function that invokes `func` with arguments reversed. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Function + * @param {Function} func The function to flip arguments for. + * @returns {Function} Returns the new flipped function. + * @example + * + * var flipped = _.flip(function() { + * return _.toArray(arguments); + * }); + * + * flipped('a', 'b', 'c', 'd'); + * // => ['d', 'c', 'b', 'a'] + */ + function flip(func) { + return createWrap(func, WRAP_FLIP_FLAG); + } + + /** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided, it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the + * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) + * method interface of `clear`, `delete`, `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoized function. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; + * + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] + * + * // Modify the result cache. + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] + * + * // Replace `_.memoize.Cache`. + * _.memoize.Cache = WeakMap; + */ + function memoize(func, resolver) { + if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result) || cache; + return result; + }; + memoized.cache = new (memoize.Cache || MapCache); + return memoized; + } + + // Expose `MapCache`. + memoize.Cache = MapCache; + + /** + * Creates a function that negates the result of the predicate `func`. The + * `func` predicate is invoked with the `this` binding and arguments of the + * created function. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} predicate The predicate to negate. + * @returns {Function} Returns the new negated function. + * @example + * + * function isEven(n) { + * return n % 2 == 0; + * } + * + * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven)); + * // => [1, 3, 5] + */ + function negate(predicate) { + if (typeof predicate != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + return function() { + var args = arguments; + switch (args.length) { + case 0: return !predicate.call(this); + case 1: return !predicate.call(this, args[0]); + case 2: return !predicate.call(this, args[0], args[1]); + case 3: return !predicate.call(this, args[0], args[1], args[2]); + } + return !predicate.apply(this, args); + }; + } + + /** + * Creates a function that is restricted to invoking `func` once. Repeat calls + * to the function return the value of the first invocation. The `func` is + * invoked with the `this` binding and arguments of the created function. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var initialize = _.once(createApplication); + * initialize(); + * initialize(); + * // => `createApplication` is invoked once + */ + function once(func) { + return before(2, func); + } + + /** + * Creates a function that invokes `func` with its arguments transformed. + * + * @static + * @since 4.0.0 + * @memberOf _ + * @category Function + * @param {Function} func The function to wrap. + * @param {...(Function|Function[])} [transforms=[_.identity]] + * The argument transforms. + * @returns {Function} Returns the new function. + * @example + * + * function doubled(n) { + * return n * 2; + * } + * + * function square(n) { + * return n * n; + * } + * + * var func = _.overArgs(function(x, y) { + * return [x, y]; + * }, [square, doubled]); + * + * func(9, 3); + * // => [81, 6] + * + * func(10, 5); + * // => [100, 10] + */ + var overArgs = castRest(function(func, transforms) { + transforms = (transforms.length == 1 && isArray(transforms[0])) + ? arrayMap(transforms[0], baseUnary(getIteratee())) + : arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee())); + + var funcsLength = transforms.length; + return baseRest(function(args) { + var index = -1, + length = nativeMin(args.length, funcsLength); + + while (++index < length) { + args[index] = transforms[index].call(this, args[index]); + } + return apply(func, this, args); + }); + }); + + /** + * Creates a function that invokes `func` with `partials` prepended to the + * arguments it receives. This method is like `_.bind` except it does **not** + * alter the `this` binding. + * + * The `_.partial.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * **Note:** This method doesn't set the "length" property of partially + * applied functions. + * + * @static + * @memberOf _ + * @since 0.2.0 + * @category Function + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * function greet(greeting, name) { + * return greeting + ' ' + name; + * } + * + * var sayHelloTo = _.partial(greet, 'hello'); + * sayHelloTo('fred'); + * // => 'hello fred' + * + * // Partially applied with placeholders. + * var greetFred = _.partial(greet, _, 'fred'); + * greetFred('hi'); + * // => 'hi fred' + */ + var partial = baseRest(function(func, partials) { + var holders = replaceHolders(partials, getHolder(partial)); + return createWrap(func, WRAP_PARTIAL_FLAG, undefined, partials, holders); + }); + + /** + * This method is like `_.partial` except that partially applied arguments + * are appended to the arguments it receives. + * + * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * **Note:** This method doesn't set the "length" property of partially + * applied functions. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Function + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * function greet(greeting, name) { + * return greeting + ' ' + name; + * } + * + * var greetFred = _.partialRight(greet, 'fred'); + * greetFred('hi'); + * // => 'hi fred' + * + * // Partially applied with placeholders. + * var sayHelloTo = _.partialRight(greet, 'hello', _); + * sayHelloTo('fred'); + * // => 'hello fred' + */ + var partialRight = baseRest(function(func, partials) { + var holders = replaceHolders(partials, getHolder(partialRight)); + return createWrap(func, WRAP_PARTIAL_RIGHT_FLAG, undefined, partials, holders); + }); + + /** + * Creates a function that invokes `func` with arguments arranged according + * to the specified `indexes` where the argument value at the first index is + * provided as the first argument, the argument value at the second index is + * provided as the second argument, and so on. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} func The function to rearrange arguments for. + * @param {...(number|number[])} indexes The arranged argument indexes. + * @returns {Function} Returns the new function. + * @example + * + * var rearged = _.rearg(function(a, b, c) { + * return [a, b, c]; + * }, [2, 0, 1]); + * + * rearged('b', 'c', 'a') + * // => ['a', 'b', 'c'] + */ + var rearg = flatRest(function(func, indexes) { + return createWrap(func, WRAP_REARG_FLAG, undefined, undefined, undefined, indexes); + }); + + /** + * Creates a function that invokes `func` with the `this` binding of the + * created function and arguments from `start` and beyond provided as + * an array. + * + * **Note:** This method is based on the + * [rest parameter](https://mdn.io/rest_parameters). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Function + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.rest(function(what, names) { + * return what + ' ' + _.initial(names).join(', ') + + * (_.size(names) > 1 ? ', & ' : '') + _.last(names); + * }); + * + * say('hello', 'fred', 'barney', 'pebbles'); + * // => 'hello fred, barney, & pebbles' + */ + function rest(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = start === undefined ? start : toInteger(start); + return baseRest(func, start); + } + + /** + * Creates a function that invokes `func` with the `this` binding of the + * create function and an array of arguments much like + * [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply). + * + * **Note:** This method is based on the + * [spread operator](https://mdn.io/spread_operator). + * + * @static + * @memberOf _ + * @since 3.2.0 + * @category Function + * @param {Function} func The function to spread arguments over. + * @param {number} [start=0] The start position of the spread. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.spread(function(who, what) { + * return who + ' says ' + what; + * }); + * + * say(['fred', 'hello']); + * // => 'fred says hello' + * + * var numbers = Promise.all([ + * Promise.resolve(40), + * Promise.resolve(36) + * ]); + * + * numbers.then(_.spread(function(x, y) { + * return x + y; + * })); + * // => a Promise of 76 + */ + function spread(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = start == null ? 0 : nativeMax(toInteger(start), 0); + return baseRest(function(args) { + var array = args[start], + otherArgs = castSlice(args, 0, start); + + if (array) { + arrayPush(otherArgs, array); + } + return apply(func, this, otherArgs); + }); + } + + /** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ + function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); + } + + /** + * Creates a function that accepts up to one argument, ignoring any + * additional arguments. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Function + * @param {Function} func The function to cap arguments for. + * @returns {Function} Returns the new capped function. + * @example + * + * _.map(['6', '8', '10'], _.unary(parseInt)); + * // => [6, 8, 10] + */ + function unary(func) { + return ary(func, 1); + } + + /** + * Creates a function that provides `value` to `wrapper` as its first + * argument. Any additional arguments provided to the function are appended + * to those provided to the `wrapper`. The wrapper is invoked with the `this` + * binding of the created function. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {*} value The value to wrap. + * @param {Function} [wrapper=identity] The wrapper function. + * @returns {Function} Returns the new function. + * @example + * + * var p = _.wrap(_.escape, function(func, text) { + * return '

' + func(text) + '

'; + * }); + * + * p('fred, barney, & pebbles'); + * // => '

fred, barney, & pebbles

' + */ + function wrap(value, wrapper) { + return partial(castFunction(wrapper), value); + } + + /*------------------------------------------------------------------------*/ + + /** + * Casts `value` as an array if it's not one. + * + * @static + * @memberOf _ + * @since 4.4.0 + * @category Lang + * @param {*} value The value to inspect. + * @returns {Array} Returns the cast array. + * @example + * + * _.castArray(1); + * // => [1] + * + * _.castArray({ 'a': 1 }); + * // => [{ 'a': 1 }] + * + * _.castArray('abc'); + * // => ['abc'] + * + * _.castArray(null); + * // => [null] + * + * _.castArray(undefined); + * // => [undefined] + * + * _.castArray(); + * // => [] + * + * var array = [1, 2, 3]; + * console.log(_.castArray(array) === array); + * // => true + */ + function castArray() { + if (!arguments.length) { + return []; + } + var value = arguments[0]; + return isArray(value) ? value : [value]; + } + + /** + * Creates a shallow clone of `value`. + * + * **Note:** This method is loosely based on the + * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm) + * and supports cloning arrays, array buffers, booleans, date objects, maps, + * numbers, `Object` objects, regexes, sets, strings, symbols, and typed + * arrays. The own enumerable properties of `arguments` objects are cloned + * as plain objects. An empty object is returned for uncloneable values such + * as error objects, functions, DOM nodes, and WeakMaps. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to clone. + * @returns {*} Returns the cloned value. + * @see _.cloneDeep + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var shallow = _.clone(objects); + * console.log(shallow[0] === objects[0]); + * // => true + */ + function clone(value) { + return baseClone(value, CLONE_SYMBOLS_FLAG); + } + + /** + * This method is like `_.clone` except that it accepts `customizer` which + * is invoked to produce the cloned value. If `customizer` returns `undefined`, + * cloning is handled by the method instead. The `customizer` is invoked with + * up to four arguments; (value [, index|key, object, stack]). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to clone. + * @param {Function} [customizer] The function to customize cloning. + * @returns {*} Returns the cloned value. + * @see _.cloneDeepWith + * @example + * + * function customizer(value) { + * if (_.isElement(value)) { + * return value.cloneNode(false); + * } + * } + * + * var el = _.cloneWith(document.body, customizer); + * + * console.log(el === document.body); + * // => false + * console.log(el.nodeName); + * // => 'BODY' + * console.log(el.childNodes.length); + * // => 0 + */ + function cloneWith(value, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return baseClone(value, CLONE_SYMBOLS_FLAG, customizer); + } + + /** + * This method is like `_.clone` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @returns {*} Returns the deep cloned value. + * @see _.clone + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var deep = _.cloneDeep(objects); + * console.log(deep[0] === objects[0]); + * // => false + */ + function cloneDeep(value) { + return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG); + } + + /** + * This method is like `_.cloneWith` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @param {Function} [customizer] The function to customize cloning. + * @returns {*} Returns the deep cloned value. + * @see _.cloneWith + * @example + * + * function customizer(value) { + * if (_.isElement(value)) { + * return value.cloneNode(true); + * } + * } + * + * var el = _.cloneDeepWith(document.body, customizer); + * + * console.log(el === document.body); + * // => false + * console.log(el.nodeName); + * // => 'BODY' + * console.log(el.childNodes.length); + * // => 20 + */ + function cloneDeepWith(value, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer); + } + + /** + * Checks if `object` conforms to `source` by invoking the predicate + * properties of `source` with the corresponding property values of `object`. + * + * **Note:** This method is equivalent to `_.conforms` when `source` is + * partially applied. + * + * @static + * @memberOf _ + * @since 4.14.0 + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property predicates to conform to. + * @returns {boolean} Returns `true` if `object` conforms, else `false`. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * + * _.conformsTo(object, { 'b': function(n) { return n > 1; } }); + * // => true + * + * _.conformsTo(object, { 'b': function(n) { return n > 2; } }); + * // => false + */ + function conformsTo(object, source) { + return source == null || baseConformsTo(object, source, keys(source)); + } + + /** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ + function eq(value, other) { + return value === other || (value !== value && other !== other); + } + + /** + * Checks if `value` is greater than `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than `other`, + * else `false`. + * @see _.lt + * @example + * + * _.gt(3, 1); + * // => true + * + * _.gt(3, 3); + * // => false + * + * _.gt(1, 3); + * // => false + */ + var gt = createRelationalOperation(baseGt); + + /** + * Checks if `value` is greater than or equal to `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than or equal to + * `other`, else `false`. + * @see _.lte + * @example + * + * _.gte(3, 1); + * // => true + * + * _.gte(3, 3); + * // => true + * + * _.gte(1, 3); + * // => false + */ + var gte = createRelationalOperation(function(value, other) { + return value >= other; + }); + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) { + return isObjectLike(value) && hasOwnProperty.call(value, 'callee') && + !propertyIsEnumerable.call(value, 'callee'); + }; + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray = Array.isArray; + + /** + * Checks if `value` is classified as an `ArrayBuffer` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. + * @example + * + * _.isArrayBuffer(new ArrayBuffer(2)); + * // => true + * + * _.isArrayBuffer(new Array(2)); + * // => false + */ + var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); + } + + /** + * Checks if `value` is classified as a boolean primitive or object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a boolean, else `false`. + * @example + * + * _.isBoolean(false); + * // => true + * + * _.isBoolean(null); + * // => false + */ + function isBoolean(value) { + return value === true || value === false || + (isObjectLike(value) && baseGetTag(value) == boolTag); + } + + /** + * Checks if `value` is a buffer. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. + * @example + * + * _.isBuffer(new Buffer(2)); + * // => true + * + * _.isBuffer(new Uint8Array(2)); + * // => false + */ + var isBuffer = nativeIsBuffer || stubFalse; + + /** + * Checks if `value` is classified as a `Date` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a date object, else `false`. + * @example + * + * _.isDate(new Date); + * // => true + * + * _.isDate('Mon April 23 2012'); + * // => false + */ + var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate; + + /** + * Checks if `value` is likely a DOM element. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`. + * @example + * + * _.isElement(document.body); + * // => true + * + * _.isElement(''); + * // => false + */ + function isElement(value) { + return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value); + } + + /** + * Checks if `value` is an empty object, collection, map, or set. + * + * Objects are considered empty if they have no own enumerable string keyed + * properties. + * + * Array-like values such as `arguments` objects, arrays, buffers, strings, or + * jQuery-like collections are considered empty if they have a `length` of `0`. + * Similarly, maps and sets are considered empty if they have a `size` of `0`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is empty, else `false`. + * @example + * + * _.isEmpty(null); + * // => true + * + * _.isEmpty(true); + * // => true + * + * _.isEmpty(1); + * // => true + * + * _.isEmpty([1, 2, 3]); + * // => false + * + * _.isEmpty({ 'a': 1 }); + * // => false + */ + function isEmpty(value) { + if (value == null) { + return true; + } + if (isArrayLike(value) && + (isArray(value) || typeof value == 'string' || typeof value.splice == 'function' || + isBuffer(value) || isTypedArray(value) || isArguments(value))) { + return !value.length; + } + var tag = getTag(value); + if (tag == mapTag || tag == setTag) { + return !value.size; + } + if (isPrototype(value)) { + return !baseKeys(value).length; + } + for (var key in value) { + if (hasOwnProperty.call(value, key)) { + return false; + } + } + return true; + } + + /** + * Performs a deep comparison between two values to determine if they are + * equivalent. + * + * **Note:** This method supports comparing arrays, array buffers, booleans, + * date objects, error objects, maps, numbers, `Object` objects, regexes, + * sets, strings, symbols, and typed arrays. `Object` objects are compared + * by their own, not inherited, enumerable properties. Functions and DOM + * nodes are compared by strict equality, i.e. `===`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.isEqual(object, other); + * // => true + * + * object === other; + * // => false + */ + function isEqual(value, other) { + return baseIsEqual(value, other); + } + + /** + * This method is like `_.isEqual` except that it accepts `customizer` which + * is invoked to compare values. If `customizer` returns `undefined`, comparisons + * are handled by the method instead. The `customizer` is invoked with up to + * six arguments: (objValue, othValue [, index|key, object, other, stack]). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * function isGreeting(value) { + * return /^h(?:i|ello)$/.test(value); + * } + * + * function customizer(objValue, othValue) { + * if (isGreeting(objValue) && isGreeting(othValue)) { + * return true; + * } + * } + * + * var array = ['hello', 'goodbye']; + * var other = ['hi', 'goodbye']; + * + * _.isEqualWith(array, other, customizer); + * // => true + */ + function isEqualWith(value, other, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + var result = customizer ? customizer(value, other) : undefined; + return result === undefined ? baseIsEqual(value, other, undefined, customizer) : !!result; + } + + /** + * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`, + * `SyntaxError`, `TypeError`, or `URIError` object. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an error object, else `false`. + * @example + * + * _.isError(new Error); + * // => true + * + * _.isError(Error); + * // => false + */ + function isError(value) { + if (!isObjectLike(value)) { + return false; + } + var tag = baseGetTag(value); + return tag == errorTag || tag == domExcTag || + (typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value)); + } + + /** + * Checks if `value` is a finite primitive number. + * + * **Note:** This method is based on + * [`Number.isFinite`](https://mdn.io/Number/isFinite). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a finite number, else `false`. + * @example + * + * _.isFinite(3); + * // => true + * + * _.isFinite(Number.MIN_VALUE); + * // => true + * + * _.isFinite(Infinity); + * // => false + * + * _.isFinite('3'); + * // => false + */ + function isFinite(value) { + return typeof value == 'number' && nativeIsFinite(value); + } + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction(value) { + if (!isObject(value)) { + return false; + } + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 9 which returns 'object' for typed arrays and other constructors. + var tag = baseGetTag(value); + return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; + } + + /** + * Checks if `value` is an integer. + * + * **Note:** This method is based on + * [`Number.isInteger`](https://mdn.io/Number/isInteger). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an integer, else `false`. + * @example + * + * _.isInteger(3); + * // => true + * + * _.isInteger(Number.MIN_VALUE); + * // => false + * + * _.isInteger(Infinity); + * // => false + * + * _.isInteger('3'); + * // => false + */ + function isInteger(value) { + return typeof value == 'number' && value == toInteger(value); + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return value != null && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Map` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a map, else `false`. + * @example + * + * _.isMap(new Map); + * // => true + * + * _.isMap(new WeakMap); + * // => false + */ + var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap; + + /** + * Performs a partial deep comparison between `object` and `source` to + * determine if `object` contains equivalent property values. + * + * **Note:** This method is equivalent to `_.matches` when `source` is + * partially applied. + * + * Partial comparisons will match empty array and empty object `source` + * values against any array or object value, respectively. See `_.isEqual` + * for a list of supported value comparisons. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * + * _.isMatch(object, { 'b': 2 }); + * // => true + * + * _.isMatch(object, { 'b': 1 }); + * // => false + */ + function isMatch(object, source) { + return object === source || baseIsMatch(object, source, getMatchData(source)); + } + + /** + * This method is like `_.isMatch` except that it accepts `customizer` which + * is invoked to compare values. If `customizer` returns `undefined`, comparisons + * are handled by the method instead. The `customizer` is invoked with five + * arguments: (objValue, srcValue, index|key, object, source). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + * @example + * + * function isGreeting(value) { + * return /^h(?:i|ello)$/.test(value); + * } + * + * function customizer(objValue, srcValue) { + * if (isGreeting(objValue) && isGreeting(srcValue)) { + * return true; + * } + * } + * + * var object = { 'greeting': 'hello' }; + * var source = { 'greeting': 'hi' }; + * + * _.isMatchWith(object, source, customizer); + * // => true + */ + function isMatchWith(object, source, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return baseIsMatch(object, source, getMatchData(source), customizer); + } + + /** + * Checks if `value` is `NaN`. + * + * **Note:** This method is based on + * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as + * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for + * `undefined` and other non-number values. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. + * @example + * + * _.isNaN(NaN); + * // => true + * + * _.isNaN(new Number(NaN)); + * // => true + * + * isNaN(undefined); + * // => true + * + * _.isNaN(undefined); + * // => false + */ + function isNaN(value) { + // An `NaN` primitive is the only value that is not equal to itself. + // Perform the `toStringTag` check first to avoid errors with some + // ActiveX objects in IE. + return isNumber(value) && value != +value; + } + + /** + * Checks if `value` is a pristine native function. + * + * **Note:** This method can't reliably detect native functions in the presence + * of the core-js package because core-js circumvents this kind of detection. + * Despite multiple requests, the core-js maintainer has made it clear: any + * attempt to fix the detection will be obstructed. As a result, we're left + * with little choice but to throw an error. Unfortunately, this also affects + * packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill), + * which rely on core-js. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + * @example + * + * _.isNative(Array.prototype.push); + * // => true + * + * _.isNative(_); + * // => false + */ + function isNative(value) { + if (isMaskable(value)) { + throw new Error(CORE_ERROR_TEXT); + } + return baseIsNative(value); + } + + /** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(void 0); + * // => false + */ + function isNull(value) { + return value === null; + } + + /** + * Checks if `value` is `null` or `undefined`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is nullish, else `false`. + * @example + * + * _.isNil(null); + * // => true + * + * _.isNil(void 0); + * // => true + * + * _.isNil(NaN); + * // => false + */ + function isNil(value) { + return value == null; + } + + /** + * Checks if `value` is classified as a `Number` primitive or object. + * + * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are + * classified as numbers, use the `_.isFinite` method. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a number, else `false`. + * @example + * + * _.isNumber(3); + * // => true + * + * _.isNumber(Number.MIN_VALUE); + * // => true + * + * _.isNumber(Infinity); + * // => true + * + * _.isNumber('3'); + * // => false + */ + function isNumber(value) { + return typeof value == 'number' || + (isObjectLike(value) && baseGetTag(value) == numberTag); + } + + /** + * Checks if `value` is a plain object, that is, an object created by the + * `Object` constructor or one with a `[[Prototype]]` of `null`. + * + * @static + * @memberOf _ + * @since 0.8.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * _.isPlainObject(new Foo); + * // => false + * + * _.isPlainObject([1, 2, 3]); + * // => false + * + * _.isPlainObject({ 'x': 0, 'y': 0 }); + * // => true + * + * _.isPlainObject(Object.create(null)); + * // => true + */ + function isPlainObject(value) { + if (!isObjectLike(value) || baseGetTag(value) != objectTag) { + return false; + } + var proto = getPrototype(value); + if (proto === null) { + return true; + } + var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor; + return typeof Ctor == 'function' && Ctor instanceof Ctor && + funcToString.call(Ctor) == objectCtorString; + } + + /** + * Checks if `value` is classified as a `RegExp` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. + * @example + * + * _.isRegExp(/abc/); + * // => true + * + * _.isRegExp('/abc/'); + * // => false + */ + var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp; + + /** + * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754 + * double precision number which isn't the result of a rounded unsafe integer. + * + * **Note:** This method is based on + * [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`. + * @example + * + * _.isSafeInteger(3); + * // => true + * + * _.isSafeInteger(Number.MIN_VALUE); + * // => false + * + * _.isSafeInteger(Infinity); + * // => false + * + * _.isSafeInteger('3'); + * // => false + */ + function isSafeInteger(value) { + return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is classified as a `Set` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a set, else `false`. + * @example + * + * _.isSet(new Set); + * // => true + * + * _.isSet(new WeakSet); + * // => false + */ + var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet; + + /** + * Checks if `value` is classified as a `String` primitive or object. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a string, else `false`. + * @example + * + * _.isString('abc'); + * // => true + * + * _.isString(1); + * // => false + */ + function isString(value) { + return typeof value == 'string' || + (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag); + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && baseGetTag(value) == symbolTag); + } + + /** + * Checks if `value` is classified as a typed array. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. + * @example + * + * _.isTypedArray(new Uint8Array); + * // => true + * + * _.isTypedArray([]); + * // => false + */ + var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray; + + /** + * Checks if `value` is `undefined`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + * + * _.isUndefined(null); + * // => false + */ + function isUndefined(value) { + return value === undefined; + } + + /** + * Checks if `value` is classified as a `WeakMap` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a weak map, else `false`. + * @example + * + * _.isWeakMap(new WeakMap); + * // => true + * + * _.isWeakMap(new Map); + * // => false + */ + function isWeakMap(value) { + return isObjectLike(value) && getTag(value) == weakMapTag; + } + + /** + * Checks if `value` is classified as a `WeakSet` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a weak set, else `false`. + * @example + * + * _.isWeakSet(new WeakSet); + * // => true + * + * _.isWeakSet(new Set); + * // => false + */ + function isWeakSet(value) { + return isObjectLike(value) && baseGetTag(value) == weakSetTag; + } + + /** + * Checks if `value` is less than `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than `other`, + * else `false`. + * @see _.gt + * @example + * + * _.lt(1, 3); + * // => true + * + * _.lt(3, 3); + * // => false + * + * _.lt(3, 1); + * // => false + */ + var lt = createRelationalOperation(baseLt); + + /** + * Checks if `value` is less than or equal to `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than or equal to + * `other`, else `false`. + * @see _.gte + * @example + * + * _.lte(1, 3); + * // => true + * + * _.lte(3, 3); + * // => true + * + * _.lte(3, 1); + * // => false + */ + var lte = createRelationalOperation(function(value, other) { + return value <= other; + }); + + /** + * Converts `value` to an array. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to convert. + * @returns {Array} Returns the converted array. + * @example + * + * _.toArray({ 'a': 1, 'b': 2 }); + * // => [1, 2] + * + * _.toArray('abc'); + * // => ['a', 'b', 'c'] + * + * _.toArray(1); + * // => [] + * + * _.toArray(null); + * // => [] + */ + function toArray(value) { + if (!value) { + return []; + } + if (isArrayLike(value)) { + return isString(value) ? stringToArray(value) : copyArray(value); + } + if (symIterator && value[symIterator]) { + return iteratorToArray(value[symIterator]()); + } + var tag = getTag(value), + func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values); + + return func(value); + } + + /** + * Converts `value` to a finite number. + * + * @static + * @memberOf _ + * @since 4.12.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted number. + * @example + * + * _.toFinite(3.2); + * // => 3.2 + * + * _.toFinite(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toFinite(Infinity); + * // => 1.7976931348623157e+308 + * + * _.toFinite('3.2'); + * // => 3.2 + */ + function toFinite(value) { + if (!value) { + return value === 0 ? value : 0; + } + value = toNumber(value); + if (value === INFINITY || value === -INFINITY) { + var sign = (value < 0 ? -1 : 1); + return sign * MAX_INTEGER; + } + return value === value ? value : 0; + } + + /** + * Converts `value` to an integer. + * + * **Note:** This method is loosely based on + * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted integer. + * @example + * + * _.toInteger(3.2); + * // => 3 + * + * _.toInteger(Number.MIN_VALUE); + * // => 0 + * + * _.toInteger(Infinity); + * // => 1.7976931348623157e+308 + * + * _.toInteger('3.2'); + * // => 3 + */ + function toInteger(value) { + var result = toFinite(value), + remainder = result % 1; + + return result === result ? (remainder ? result - remainder : result) : 0; + } + + /** + * Converts `value` to an integer suitable for use as the length of an + * array-like object. + * + * **Note:** This method is based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted integer. + * @example + * + * _.toLength(3.2); + * // => 3 + * + * _.toLength(Number.MIN_VALUE); + * // => 0 + * + * _.toLength(Infinity); + * // => 4294967295 + * + * _.toLength('3.2'); + * // => 3 + */ + function toLength(value) { + return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0; + } + + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); + } + + /** + * Converts `value` to a plain object flattening inherited enumerable string + * keyed properties of `value` to own properties of the plain object. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {Object} Returns the converted plain object. + * @example + * + * function Foo() { + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.assign({ 'a': 1 }, new Foo); + * // => { 'a': 1, 'b': 2 } + * + * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); + * // => { 'a': 1, 'b': 2, 'c': 3 } + */ + function toPlainObject(value) { + return copyObject(value, keysIn(value)); + } + + /** + * Converts `value` to a safe integer. A safe integer can be compared and + * represented correctly. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted integer. + * @example + * + * _.toSafeInteger(3.2); + * // => 3 + * + * _.toSafeInteger(Number.MIN_VALUE); + * // => 0 + * + * _.toSafeInteger(Infinity); + * // => 9007199254740991 + * + * _.toSafeInteger('3.2'); + * // => 3 + */ + function toSafeInteger(value) { + return value + ? baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER) + : (value === 0 ? value : 0); + } + + /** + * Converts `value` to a string. An empty string is returned for `null` + * and `undefined` values. The sign of `-0` is preserved. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.toString(null); + * // => '' + * + * _.toString(-0); + * // => '-0' + * + * _.toString([1, 2, 3]); + * // => '1,2,3' + */ + function toString(value) { + return value == null ? '' : baseToString(value); + } + + /*------------------------------------------------------------------------*/ + + /** + * Assigns own enumerable string keyed properties of source objects to the + * destination object. Source objects are applied from left to right. + * Subsequent sources overwrite property assignments of previous sources. + * + * **Note:** This method mutates `object` and is loosely based on + * [`Object.assign`](https://mdn.io/Object/assign). + * + * @static + * @memberOf _ + * @since 0.10.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.assignIn + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * function Bar() { + * this.c = 3; + * } + * + * Foo.prototype.b = 2; + * Bar.prototype.d = 4; + * + * _.assign({ 'a': 0 }, new Foo, new Bar); + * // => { 'a': 1, 'c': 3 } + */ + var assign = createAssigner(function(object, source) { + if (isPrototype(source) || isArrayLike(source)) { + copyObject(source, keys(source), object); + return; + } + for (var key in source) { + if (hasOwnProperty.call(source, key)) { + assignValue(object, key, source[key]); + } + } + }); + + /** + * This method is like `_.assign` except that it iterates over own and + * inherited source properties. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias extend + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.assign + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * function Bar() { + * this.c = 3; + * } + * + * Foo.prototype.b = 2; + * Bar.prototype.d = 4; + * + * _.assignIn({ 'a': 0 }, new Foo, new Bar); + * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 } + */ + var assignIn = createAssigner(function(object, source) { + copyObject(source, keysIn(source), object); + }); + + /** + * This method is like `_.assignIn` except that it accepts `customizer` + * which is invoked to produce the assigned values. If `customizer` returns + * `undefined`, assignment is handled by the method instead. The `customizer` + * is invoked with five arguments: (objValue, srcValue, key, object, source). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias extendWith + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @see _.assignWith + * @example + * + * function customizer(objValue, srcValue) { + * return _.isUndefined(objValue) ? srcValue : objValue; + * } + * + * var defaults = _.partialRight(_.assignInWith, customizer); + * + * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); + * // => { 'a': 1, 'b': 2 } + */ + var assignInWith = createAssigner(function(object, source, srcIndex, customizer) { + copyObject(source, keysIn(source), object, customizer); + }); + + /** + * This method is like `_.assign` except that it accepts `customizer` + * which is invoked to produce the assigned values. If `customizer` returns + * `undefined`, assignment is handled by the method instead. The `customizer` + * is invoked with five arguments: (objValue, srcValue, key, object, source). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @see _.assignInWith + * @example + * + * function customizer(objValue, srcValue) { + * return _.isUndefined(objValue) ? srcValue : objValue; + * } + * + * var defaults = _.partialRight(_.assignWith, customizer); + * + * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); + * // => { 'a': 1, 'b': 2 } + */ + var assignWith = createAssigner(function(object, source, srcIndex, customizer) { + copyObject(source, keys(source), object, customizer); + }); + + /** + * Creates an array of values corresponding to `paths` of `object`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {...(string|string[])} [paths] The property paths to pick. + * @returns {Array} Returns the picked values. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; + * + * _.at(object, ['a[0].b.c', 'a[1]']); + * // => [3, 4] + */ + var at = flatRest(baseAt); + + /** + * Creates an object that inherits from the `prototype` object. If a + * `properties` object is given, its own enumerable string keyed properties + * are assigned to the created object. + * + * @static + * @memberOf _ + * @since 2.3.0 + * @category Object + * @param {Object} prototype The object to inherit from. + * @param {Object} [properties] The properties to assign to the object. + * @returns {Object} Returns the new object. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * function Circle() { + * Shape.call(this); + * } + * + * Circle.prototype = _.create(Shape.prototype, { + * 'constructor': Circle + * }); + * + * var circle = new Circle; + * circle instanceof Circle; + * // => true + * + * circle instanceof Shape; + * // => true + */ + function create(prototype, properties) { + var result = baseCreate(prototype); + return properties == null ? result : baseAssign(result, properties); + } + + /** + * Assigns own and inherited enumerable string keyed properties of source + * objects to the destination object for all destination properties that + * resolve to `undefined`. Source objects are applied from left to right. + * Once a property is set, additional values of the same property are ignored. + * + * **Note:** This method mutates `object`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.defaultsDeep + * @example + * + * _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); + * // => { 'a': 1, 'b': 2 } + */ + var defaults = baseRest(function(object, sources) { + object = Object(object); + + var index = -1; + var length = sources.length; + var guard = length > 2 ? sources[2] : undefined; + + if (guard && isIterateeCall(sources[0], sources[1], guard)) { + length = 1; + } + + while (++index < length) { + var source = sources[index]; + var props = keysIn(source); + var propsIndex = -1; + var propsLength = props.length; + + while (++propsIndex < propsLength) { + var key = props[propsIndex]; + var value = object[key]; + + if (value === undefined || + (eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))) { + object[key] = source[key]; + } + } + } + + return object; + }); + + /** + * This method is like `_.defaults` except that it recursively assigns + * default properties. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 3.10.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.defaults + * @example + * + * _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } }); + * // => { 'a': { 'b': 2, 'c': 3 } } + */ + var defaultsDeep = baseRest(function(args) { + args.push(undefined, customDefaultsMerge); + return apply(mergeWith, undefined, args); + }); + + /** + * This method is like `_.find` except that it returns the key of the first + * element `predicate` returns truthy for instead of the element itself. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category Object + * @param {Object} object The object to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {string|undefined} Returns the key of the matched element, + * else `undefined`. + * @example + * + * var users = { + * 'barney': { 'age': 36, 'active': true }, + * 'fred': { 'age': 40, 'active': false }, + * 'pebbles': { 'age': 1, 'active': true } + * }; + * + * _.findKey(users, function(o) { return o.age < 40; }); + * // => 'barney' (iteration order is not guaranteed) + * + * // The `_.matches` iteratee shorthand. + * _.findKey(users, { 'age': 1, 'active': true }); + * // => 'pebbles' + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findKey(users, ['active', false]); + * // => 'fred' + * + * // The `_.property` iteratee shorthand. + * _.findKey(users, 'active'); + * // => 'barney' + */ + function findKey(object, predicate) { + return baseFindKey(object, getIteratee(predicate, 3), baseForOwn); + } + + /** + * This method is like `_.findKey` except that it iterates over elements of + * a collection in the opposite order. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Object + * @param {Object} object The object to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {string|undefined} Returns the key of the matched element, + * else `undefined`. + * @example + * + * var users = { + * 'barney': { 'age': 36, 'active': true }, + * 'fred': { 'age': 40, 'active': false }, + * 'pebbles': { 'age': 1, 'active': true } + * }; + * + * _.findLastKey(users, function(o) { return o.age < 40; }); + * // => returns 'pebbles' assuming `_.findKey` returns 'barney' + * + * // The `_.matches` iteratee shorthand. + * _.findLastKey(users, { 'age': 36, 'active': true }); + * // => 'barney' + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findLastKey(users, ['active', false]); + * // => 'fred' + * + * // The `_.property` iteratee shorthand. + * _.findLastKey(users, 'active'); + * // => 'pebbles' + */ + function findLastKey(object, predicate) { + return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight); + } + + /** + * Iterates over own and inherited enumerable string keyed properties of an + * object and invokes `iteratee` for each property. The iteratee is invoked + * with three arguments: (value, key, object). Iteratee functions may exit + * iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @since 0.3.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forInRight + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forIn(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed). + */ + function forIn(object, iteratee) { + return object == null + ? object + : baseFor(object, getIteratee(iteratee, 3), keysIn); + } + + /** + * This method is like `_.forIn` except that it iterates over properties of + * `object` in the opposite order. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forIn + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forInRight(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'. + */ + function forInRight(object, iteratee) { + return object == null + ? object + : baseForRight(object, getIteratee(iteratee, 3), keysIn); + } + + /** + * Iterates over own enumerable string keyed properties of an object and + * invokes `iteratee` for each property. The iteratee is invoked with three + * arguments: (value, key, object). Iteratee functions may exit iteration + * early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @since 0.3.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forOwnRight + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forOwn(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a' then 'b' (iteration order is not guaranteed). + */ + function forOwn(object, iteratee) { + return object && baseForOwn(object, getIteratee(iteratee, 3)); + } + + /** + * This method is like `_.forOwn` except that it iterates over properties of + * `object` in the opposite order. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forOwn + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forOwnRight(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'. + */ + function forOwnRight(object, iteratee) { + return object && baseForOwnRight(object, getIteratee(iteratee, 3)); + } + + /** + * Creates an array of function property names from own enumerable properties + * of `object`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to inspect. + * @returns {Array} Returns the function names. + * @see _.functionsIn + * @example + * + * function Foo() { + * this.a = _.constant('a'); + * this.b = _.constant('b'); + * } + * + * Foo.prototype.c = _.constant('c'); + * + * _.functions(new Foo); + * // => ['a', 'b'] + */ + function functions(object) { + return object == null ? [] : baseFunctions(object, keys(object)); + } + + /** + * Creates an array of function property names from own and inherited + * enumerable properties of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to inspect. + * @returns {Array} Returns the function names. + * @see _.functions + * @example + * + * function Foo() { + * this.a = _.constant('a'); + * this.b = _.constant('b'); + * } + * + * Foo.prototype.c = _.constant('c'); + * + * _.functionsIn(new Foo); + * // => ['a', 'b', 'c'] + */ + function functionsIn(object) { + return object == null ? [] : baseFunctions(object, keysIn(object)); + } + + /** + * Gets the value at `path` of `object`. If the resolved value is + * `undefined`, the `defaultValue` is returned in its place. + * + * @static + * @memberOf _ + * @since 3.7.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @param {*} [defaultValue] The value returned for `undefined` resolved values. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.get(object, 'a[0].b.c'); + * // => 3 + * + * _.get(object, ['a', '0', 'b', 'c']); + * // => 3 + * + * _.get(object, 'a.b.c', 'default'); + * // => 'default' + */ + function get(object, path, defaultValue) { + var result = object == null ? undefined : baseGet(object, path); + return result === undefined ? defaultValue : result; + } + + /** + * Checks if `path` is a direct property of `object`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + * @example + * + * var object = { 'a': { 'b': 2 } }; + * var other = _.create({ 'a': _.create({ 'b': 2 }) }); + * + * _.has(object, 'a'); + * // => true + * + * _.has(object, 'a.b'); + * // => true + * + * _.has(object, ['a', 'b']); + * // => true + * + * _.has(other, 'a'); + * // => false + */ + function has(object, path) { + return object != null && hasPath(object, path, baseHas); + } + + /** + * Checks if `path` is a direct or inherited property of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + * @example + * + * var object = _.create({ 'a': _.create({ 'b': 2 }) }); + * + * _.hasIn(object, 'a'); + * // => true + * + * _.hasIn(object, 'a.b'); + * // => true + * + * _.hasIn(object, ['a', 'b']); + * // => true + * + * _.hasIn(object, 'b'); + * // => false + */ + function hasIn(object, path) { + return object != null && hasPath(object, path, baseHasIn); + } + + /** + * Creates an object composed of the inverted keys and values of `object`. + * If `object` contains duplicate values, subsequent values overwrite + * property assignments of previous values. + * + * @static + * @memberOf _ + * @since 0.7.0 + * @category Object + * @param {Object} object The object to invert. + * @returns {Object} Returns the new inverted object. + * @example + * + * var object = { 'a': 1, 'b': 2, 'c': 1 }; + * + * _.invert(object); + * // => { '1': 'c', '2': 'b' } + */ + var invert = createInverter(function(result, value, key) { + if (value != null && + typeof value.toString != 'function') { + value = nativeObjectToString.call(value); + } + + result[value] = key; + }, constant(identity)); + + /** + * This method is like `_.invert` except that the inverted object is generated + * from the results of running each element of `object` thru `iteratee`. The + * corresponding inverted value of each inverted key is an array of keys + * responsible for generating the inverted value. The iteratee is invoked + * with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.1.0 + * @category Object + * @param {Object} object The object to invert. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Object} Returns the new inverted object. + * @example + * + * var object = { 'a': 1, 'b': 2, 'c': 1 }; + * + * _.invertBy(object); + * // => { '1': ['a', 'c'], '2': ['b'] } + * + * _.invertBy(object, function(value) { + * return 'group' + value; + * }); + * // => { 'group1': ['a', 'c'], 'group2': ['b'] } + */ + var invertBy = createInverter(function(result, value, key) { + if (value != null && + typeof value.toString != 'function') { + value = nativeObjectToString.call(value); + } + + if (hasOwnProperty.call(result, value)) { + result[value].push(key); + } else { + result[value] = [key]; + } + }, getIteratee); + + /** + * Invokes the method at `path` of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the method to invoke. + * @param {...*} [args] The arguments to invoke the method with. + * @returns {*} Returns the result of the invoked method. + * @example + * + * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] }; + * + * _.invoke(object, 'a[0].b.c.slice', 1, 3); + * // => [2, 3] + */ + var invoke = baseRest(baseInvoke); + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); + } + + /** + * Creates an array of the own and inherited enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keysIn(new Foo); + * // => ['a', 'b', 'c'] (iteration order is not guaranteed) + */ + function keysIn(object) { + return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object); + } + + /** + * The opposite of `_.mapValues`; this method creates an object with the + * same values as `object` and keys generated by running each own enumerable + * string keyed property of `object` thru `iteratee`. The iteratee is invoked + * with three arguments: (value, key, object). + * + * @static + * @memberOf _ + * @since 3.8.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns the new mapped object. + * @see _.mapValues + * @example + * + * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) { + * return key + value; + * }); + * // => { 'a1': 1, 'b2': 2 } + */ + function mapKeys(object, iteratee) { + var result = {}; + iteratee = getIteratee(iteratee, 3); + + baseForOwn(object, function(value, key, object) { + baseAssignValue(result, iteratee(value, key, object), value); + }); + return result; + } + + /** + * Creates an object with the same keys as `object` and values generated + * by running each own enumerable string keyed property of `object` thru + * `iteratee`. The iteratee is invoked with three arguments: + * (value, key, object). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns the new mapped object. + * @see _.mapKeys + * @example + * + * var users = { + * 'fred': { 'user': 'fred', 'age': 40 }, + * 'pebbles': { 'user': 'pebbles', 'age': 1 } + * }; + * + * _.mapValues(users, function(o) { return o.age; }); + * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) + * + * // The `_.property` iteratee shorthand. + * _.mapValues(users, 'age'); + * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) + */ + function mapValues(object, iteratee) { + var result = {}; + iteratee = getIteratee(iteratee, 3); + + baseForOwn(object, function(value, key, object) { + baseAssignValue(result, key, iteratee(value, key, object)); + }); + return result; + } + + /** + * This method is like `_.assign` except that it recursively merges own and + * inherited enumerable string keyed properties of source objects into the + * destination object. Source properties that resolve to `undefined` are + * skipped if a destination value exists. Array and plain object properties + * are merged recursively. Other objects and value types are overridden by + * assignment. Source objects are applied from left to right. Subsequent + * sources overwrite property assignments of previous sources. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 0.5.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @example + * + * var object = { + * 'a': [{ 'b': 2 }, { 'd': 4 }] + * }; + * + * var other = { + * 'a': [{ 'c': 3 }, { 'e': 5 }] + * }; + * + * _.merge(object, other); + * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] } + */ + var merge = createAssigner(function(object, source, srcIndex) { + baseMerge(object, source, srcIndex); + }); + + /** + * This method is like `_.merge` except that it accepts `customizer` which + * is invoked to produce the merged values of the destination and source + * properties. If `customizer` returns `undefined`, merging is handled by the + * method instead. The `customizer` is invoked with six arguments: + * (objValue, srcValue, key, object, source, stack). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} customizer The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * function customizer(objValue, srcValue) { + * if (_.isArray(objValue)) { + * return objValue.concat(srcValue); + * } + * } + * + * var object = { 'a': [1], 'b': [2] }; + * var other = { 'a': [3], 'b': [4] }; + * + * _.mergeWith(object, other, customizer); + * // => { 'a': [1, 3], 'b': [2, 4] } + */ + var mergeWith = createAssigner(function(object, source, srcIndex, customizer) { + baseMerge(object, source, srcIndex, customizer); + }); + + /** + * The opposite of `_.pick`; this method creates an object composed of the + * own and inherited enumerable property paths of `object` that are not omitted. + * + * **Note:** This method is considerably slower than `_.pick`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The source object. + * @param {...(string|string[])} [paths] The property paths to omit. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.omit(object, ['a', 'c']); + * // => { 'b': '2' } + */ + var omit = flatRest(function(object, paths) { + var result = {}; + if (object == null) { + return result; + } + var isDeep = false; + paths = arrayMap(paths, function(path) { + path = castPath(path, object); + isDeep || (isDeep = path.length > 1); + return path; + }); + copyObject(object, getAllKeysIn(object), result); + if (isDeep) { + result = baseClone(result, CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG, customOmitClone); + } + var length = paths.length; + while (length--) { + baseUnset(result, paths[length]); + } + return result; + }); + + /** + * The opposite of `_.pickBy`; this method creates an object composed of + * the own and inherited enumerable string keyed properties of `object` that + * `predicate` doesn't return truthy for. The predicate is invoked with two + * arguments: (value, key). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The source object. + * @param {Function} [predicate=_.identity] The function invoked per property. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.omitBy(object, _.isNumber); + * // => { 'b': '2' } + */ + function omitBy(object, predicate) { + return pickBy(object, negate(getIteratee(predicate))); + } + + /** + * Creates an object composed of the picked `object` properties. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The source object. + * @param {...(string|string[])} [paths] The property paths to pick. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.pick(object, ['a', 'c']); + * // => { 'a': 1, 'c': 3 } + */ + var pick = flatRest(function(object, paths) { + return object == null ? {} : basePick(object, paths); + }); + + /** + * Creates an object composed of the `object` properties `predicate` returns + * truthy for. The predicate is invoked with two arguments: (value, key). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The source object. + * @param {Function} [predicate=_.identity] The function invoked per property. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.pickBy(object, _.isNumber); + * // => { 'a': 1, 'c': 3 } + */ + function pickBy(object, predicate) { + if (object == null) { + return {}; + } + var props = arrayMap(getAllKeysIn(object), function(prop) { + return [prop]; + }); + predicate = getIteratee(predicate); + return basePickBy(object, props, function(value, path) { + return predicate(value, path[0]); + }); + } + + /** + * This method is like `_.get` except that if the resolved value is a + * function it's invoked with the `this` binding of its parent object and + * its result is returned. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to resolve. + * @param {*} [defaultValue] The value returned for `undefined` resolved values. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] }; + * + * _.result(object, 'a[0].b.c1'); + * // => 3 + * + * _.result(object, 'a[0].b.c2'); + * // => 4 + * + * _.result(object, 'a[0].b.c3', 'default'); + * // => 'default' + * + * _.result(object, 'a[0].b.c3', _.constant('default')); + * // => 'default' + */ + function result(object, path, defaultValue) { + path = castPath(path, object); + + var index = -1, + length = path.length; + + // Ensure the loop is entered when path is empty. + if (!length) { + length = 1; + object = undefined; + } + while (++index < length) { + var value = object == null ? undefined : object[toKey(path[index])]; + if (value === undefined) { + index = length; + value = defaultValue; + } + object = isFunction(value) ? value.call(object) : value; + } + return object; + } + + /** + * Sets the value at `path` of `object`. If a portion of `path` doesn't exist, + * it's created. Arrays are created for missing index properties while objects + * are created for all other missing properties. Use `_.setWith` to customize + * `path` creation. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 3.7.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @returns {Object} Returns `object`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.set(object, 'a[0].b.c', 4); + * console.log(object.a[0].b.c); + * // => 4 + * + * _.set(object, ['x', '0', 'y', 'z'], 5); + * console.log(object.x[0].y.z); + * // => 5 + */ + function set(object, path, value) { + return object == null ? object : baseSet(object, path, value); + } + + /** + * This method is like `_.set` except that it accepts `customizer` which is + * invoked to produce the objects of `path`. If `customizer` returns `undefined` + * path creation is handled by the method instead. The `customizer` is invoked + * with three arguments: (nsValue, key, nsObject). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var object = {}; + * + * _.setWith(object, '[0][1]', 'a', Object); + * // => { '0': { '1': 'a' } } + */ + function setWith(object, path, value, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return object == null ? object : baseSet(object, path, value, customizer); + } + + /** + * Creates an array of own enumerable string keyed-value pairs for `object` + * which can be consumed by `_.fromPairs`. If `object` is a map or set, its + * entries are returned. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias entries + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the key-value pairs. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.toPairs(new Foo); + * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed) + */ + var toPairs = createToPairs(keys); + + /** + * Creates an array of own and inherited enumerable string keyed-value pairs + * for `object` which can be consumed by `_.fromPairs`. If `object` is a map + * or set, its entries are returned. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias entriesIn + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the key-value pairs. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.toPairsIn(new Foo); + * // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed) + */ + var toPairsIn = createToPairs(keysIn); + + /** + * An alternative to `_.reduce`; this method transforms `object` to a new + * `accumulator` object which is the result of running each of its own + * enumerable string keyed properties thru `iteratee`, with each invocation + * potentially mutating the `accumulator` object. If `accumulator` is not + * provided, a new object with the same `[[Prototype]]` will be used. The + * iteratee is invoked with four arguments: (accumulator, value, key, object). + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @since 1.3.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The custom accumulator value. + * @returns {*} Returns the accumulated value. + * @example + * + * _.transform([2, 3, 4], function(result, n) { + * result.push(n *= n); + * return n % 2 == 0; + * }, []); + * // => [4, 9] + * + * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { + * (result[value] || (result[value] = [])).push(key); + * }, {}); + * // => { '1': ['a', 'c'], '2': ['b'] } + */ + function transform(object, iteratee, accumulator) { + var isArr = isArray(object), + isArrLike = isArr || isBuffer(object) || isTypedArray(object); + + iteratee = getIteratee(iteratee, 4); + if (accumulator == null) { + var Ctor = object && object.constructor; + if (isArrLike) { + accumulator = isArr ? new Ctor : []; + } + else if (isObject(object)) { + accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {}; + } + else { + accumulator = {}; + } + } + (isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) { + return iteratee(accumulator, value, index, object); + }); + return accumulator; + } + + /** + * Removes the property at `path` of `object`. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to unset. + * @returns {boolean} Returns `true` if the property is deleted, else `false`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 7 } }] }; + * _.unset(object, 'a[0].b.c'); + * // => true + * + * console.log(object); + * // => { 'a': [{ 'b': {} }] }; + * + * _.unset(object, ['a', '0', 'b', 'c']); + * // => true + * + * console.log(object); + * // => { 'a': [{ 'b': {} }] }; + */ + function unset(object, path) { + return object == null ? true : baseUnset(object, path); + } + + /** + * This method is like `_.set` except that accepts `updater` to produce the + * value to set. Use `_.updateWith` to customize `path` creation. The `updater` + * is invoked with one argument: (value). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.6.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {Function} updater The function to produce the updated value. + * @returns {Object} Returns `object`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.update(object, 'a[0].b.c', function(n) { return n * n; }); + * console.log(object.a[0].b.c); + * // => 9 + * + * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; }); + * console.log(object.x[0].y.z); + * // => 0 + */ + function update(object, path, updater) { + return object == null ? object : baseUpdate(object, path, castFunction(updater)); + } + + /** + * This method is like `_.update` except that it accepts `customizer` which is + * invoked to produce the objects of `path`. If `customizer` returns `undefined` + * path creation is handled by the method instead. The `customizer` is invoked + * with three arguments: (nsValue, key, nsObject). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.6.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {Function} updater The function to produce the updated value. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var object = {}; + * + * _.updateWith(object, '[0][1]', _.constant('a'), Object); + * // => { '0': { '1': 'a' } } + */ + function updateWith(object, path, updater, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer); + } + + /** + * Creates an array of the own enumerable string keyed property values of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.values(new Foo); + * // => [1, 2] (iteration order is not guaranteed) + * + * _.values('hi'); + * // => ['h', 'i'] + */ + function values(object) { + return object == null ? [] : baseValues(object, keys(object)); + } + + /** + * Creates an array of the own and inherited enumerable string keyed property + * values of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.valuesIn(new Foo); + * // => [1, 2, 3] (iteration order is not guaranteed) + */ + function valuesIn(object) { + return object == null ? [] : baseValues(object, keysIn(object)); + } + + /*------------------------------------------------------------------------*/ + + /** + * Clamps `number` within the inclusive `lower` and `upper` bounds. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Number + * @param {number} number The number to clamp. + * @param {number} [lower] The lower bound. + * @param {number} upper The upper bound. + * @returns {number} Returns the clamped number. + * @example + * + * _.clamp(-10, -5, 5); + * // => -5 + * + * _.clamp(10, -5, 5); + * // => 5 + */ + function clamp(number, lower, upper) { + if (upper === undefined) { + upper = lower; + lower = undefined; + } + if (upper !== undefined) { + upper = toNumber(upper); + upper = upper === upper ? upper : 0; + } + if (lower !== undefined) { + lower = toNumber(lower); + lower = lower === lower ? lower : 0; + } + return baseClamp(toNumber(number), lower, upper); + } + + /** + * Checks if `n` is between `start` and up to, but not including, `end`. If + * `end` is not specified, it's set to `start` with `start` then set to `0`. + * If `start` is greater than `end` the params are swapped to support + * negative ranges. + * + * @static + * @memberOf _ + * @since 3.3.0 + * @category Number + * @param {number} number The number to check. + * @param {number} [start=0] The start of the range. + * @param {number} end The end of the range. + * @returns {boolean} Returns `true` if `number` is in the range, else `false`. + * @see _.range, _.rangeRight + * @example + * + * _.inRange(3, 2, 4); + * // => true + * + * _.inRange(4, 8); + * // => true + * + * _.inRange(4, 2); + * // => false + * + * _.inRange(2, 2); + * // => false + * + * _.inRange(1.2, 2); + * // => true + * + * _.inRange(5.2, 4); + * // => false + * + * _.inRange(-3, -2, -6); + * // => true + */ + function inRange(number, start, end) { + start = toFinite(start); + if (end === undefined) { + end = start; + start = 0; + } else { + end = toFinite(end); + } + number = toNumber(number); + return baseInRange(number, start, end); + } + + /** + * Produces a random number between the inclusive `lower` and `upper` bounds. + * If only one argument is provided a number between `0` and the given number + * is returned. If `floating` is `true`, or either `lower` or `upper` are + * floats, a floating-point number is returned instead of an integer. + * + * **Note:** JavaScript follows the IEEE-754 standard for resolving + * floating-point values which can produce unexpected results. + * + * @static + * @memberOf _ + * @since 0.7.0 + * @category Number + * @param {number} [lower=0] The lower bound. + * @param {number} [upper=1] The upper bound. + * @param {boolean} [floating] Specify returning a floating-point number. + * @returns {number} Returns the random number. + * @example + * + * _.random(0, 5); + * // => an integer between 0 and 5 + * + * _.random(5); + * // => also an integer between 0 and 5 + * + * _.random(5, true); + * // => a floating-point number between 0 and 5 + * + * _.random(1.2, 5.2); + * // => a floating-point number between 1.2 and 5.2 + */ + function random(lower, upper, floating) { + if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) { + upper = floating = undefined; + } + if (floating === undefined) { + if (typeof upper == 'boolean') { + floating = upper; + upper = undefined; + } + else if (typeof lower == 'boolean') { + floating = lower; + lower = undefined; + } + } + if (lower === undefined && upper === undefined) { + lower = 0; + upper = 1; + } + else { + lower = toFinite(lower); + if (upper === undefined) { + upper = lower; + lower = 0; + } else { + upper = toFinite(upper); + } + } + if (lower > upper) { + var temp = lower; + lower = upper; + upper = temp; + } + if (floating || lower % 1 || upper % 1) { + var rand = nativeRandom(); + return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper); + } + return baseRandom(lower, upper); + } + + /*------------------------------------------------------------------------*/ + + /** + * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the camel cased string. + * @example + * + * _.camelCase('Foo Bar'); + * // => 'fooBar' + * + * _.camelCase('--foo-bar--'); + * // => 'fooBar' + * + * _.camelCase('__FOO_BAR__'); + * // => 'fooBar' + */ + var camelCase = createCompounder(function(result, word, index) { + word = word.toLowerCase(); + return result + (index ? capitalize(word) : word); + }); + + /** + * Converts the first character of `string` to upper case and the remaining + * to lower case. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to capitalize. + * @returns {string} Returns the capitalized string. + * @example + * + * _.capitalize('FRED'); + * // => 'Fred' + */ + function capitalize(string) { + return upperFirst(toString(string).toLowerCase()); + } + + /** + * Deburrs `string` by converting + * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) + * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) + * letters to basic Latin letters and removing + * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to deburr. + * @returns {string} Returns the deburred string. + * @example + * + * _.deburr('déjà vu'); + * // => 'deja vu' + */ + function deburr(string) { + string = toString(string); + return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); + } + + /** + * Checks if `string` ends with the given target string. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to inspect. + * @param {string} [target] The string to search for. + * @param {number} [position=string.length] The position to search up to. + * @returns {boolean} Returns `true` if `string` ends with `target`, + * else `false`. + * @example + * + * _.endsWith('abc', 'c'); + * // => true + * + * _.endsWith('abc', 'b'); + * // => false + * + * _.endsWith('abc', 'b', 2); + * // => true + */ + function endsWith(string, target, position) { + string = toString(string); + target = baseToString(target); + + var length = string.length; + position = position === undefined + ? length + : baseClamp(toInteger(position), 0, length); + + var end = position; + position -= target.length; + return position >= 0 && string.slice(position, end) == target; + } + + /** + * Converts the characters "&", "<", ">", '"', and "'" in `string` to their + * corresponding HTML entities. + * + * **Note:** No other characters are escaped. To escape additional + * characters use a third-party library like [_he_](https://mths.be/he). + * + * Though the ">" character is escaped for symmetry, characters like + * ">" and "/" don't need escaping in HTML and have no special meaning + * unless they're part of a tag or unquoted attribute value. See + * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) + * (under "semi-related fun fact") for more details. + * + * When working with HTML you should always + * [quote attribute values](http://wonko.com/post/html-escaping) to reduce + * XSS vectors. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category String + * @param {string} [string=''] The string to escape. + * @returns {string} Returns the escaped string. + * @example + * + * _.escape('fred, barney, & pebbles'); + * // => 'fred, barney, & pebbles' + */ + function escape(string) { + string = toString(string); + return (string && reHasUnescapedHtml.test(string)) + ? string.replace(reUnescapedHtml, escapeHtmlChar) + : string; + } + + /** + * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", + * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to escape. + * @returns {string} Returns the escaped string. + * @example + * + * _.escapeRegExp('[lodash](https://lodash.com/)'); + * // => '\[lodash\]\(https://lodash\.com/\)' + */ + function escapeRegExp(string) { + string = toString(string); + return (string && reHasRegExpChar.test(string)) + ? string.replace(reRegExpChar, '\\$&') + : string; + } + + /** + * Converts `string` to + * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the kebab cased string. + * @example + * + * _.kebabCase('Foo Bar'); + * // => 'foo-bar' + * + * _.kebabCase('fooBar'); + * // => 'foo-bar' + * + * _.kebabCase('__FOO_BAR__'); + * // => 'foo-bar' + */ + var kebabCase = createCompounder(function(result, word, index) { + return result + (index ? '-' : '') + word.toLowerCase(); + }); + + /** + * Converts `string`, as space separated words, to lower case. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the lower cased string. + * @example + * + * _.lowerCase('--Foo-Bar--'); + * // => 'foo bar' + * + * _.lowerCase('fooBar'); + * // => 'foo bar' + * + * _.lowerCase('__FOO_BAR__'); + * // => 'foo bar' + */ + var lowerCase = createCompounder(function(result, word, index) { + return result + (index ? ' ' : '') + word.toLowerCase(); + }); + + /** + * Converts the first character of `string` to lower case. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.lowerFirst('Fred'); + * // => 'fred' + * + * _.lowerFirst('FRED'); + * // => 'fRED' + */ + var lowerFirst = createCaseFirst('toLowerCase'); + + /** + * Pads `string` on the left and right sides if it's shorter than `length`. + * Padding characters are truncated if they can't be evenly divided by `length`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.pad('abc', 8); + * // => ' abc ' + * + * _.pad('abc', 8, '_-'); + * // => '_-abc_-_' + * + * _.pad('abc', 3); + * // => 'abc' + */ + function pad(string, length, chars) { + string = toString(string); + length = toInteger(length); + + var strLength = length ? stringSize(string) : 0; + if (!length || strLength >= length) { + return string; + } + var mid = (length - strLength) / 2; + return ( + createPadding(nativeFloor(mid), chars) + + string + + createPadding(nativeCeil(mid), chars) + ); + } + + /** + * Pads `string` on the right side if it's shorter than `length`. Padding + * characters are truncated if they exceed `length`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.padEnd('abc', 6); + * // => 'abc ' + * + * _.padEnd('abc', 6, '_-'); + * // => 'abc_-_' + * + * _.padEnd('abc', 3); + * // => 'abc' + */ + function padEnd(string, length, chars) { + string = toString(string); + length = toInteger(length); + + var strLength = length ? stringSize(string) : 0; + return (length && strLength < length) + ? (string + createPadding(length - strLength, chars)) + : string; + } + + /** + * Pads `string` on the left side if it's shorter than `length`. Padding + * characters are truncated if they exceed `length`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.padStart('abc', 6); + * // => ' abc' + * + * _.padStart('abc', 6, '_-'); + * // => '_-_abc' + * + * _.padStart('abc', 3); + * // => 'abc' + */ + function padStart(string, length, chars) { + string = toString(string); + length = toInteger(length); + + var strLength = length ? stringSize(string) : 0; + return (length && strLength < length) + ? (createPadding(length - strLength, chars) + string) + : string; + } + + /** + * Converts `string` to an integer of the specified radix. If `radix` is + * `undefined` or `0`, a `radix` of `10` is used unless `value` is a + * hexadecimal, in which case a `radix` of `16` is used. + * + * **Note:** This method aligns with the + * [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category String + * @param {string} string The string to convert. + * @param {number} [radix=10] The radix to interpret `value` by. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {number} Returns the converted integer. + * @example + * + * _.parseInt('08'); + * // => 8 + * + * _.map(['6', '08', '10'], _.parseInt); + * // => [6, 8, 10] + */ + function parseInt(string, radix, guard) { + if (guard || radix == null) { + radix = 0; + } else if (radix) { + radix = +radix; + } + return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0); + } + + /** + * Repeats the given string `n` times. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to repeat. + * @param {number} [n=1] The number of times to repeat the string. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {string} Returns the repeated string. + * @example + * + * _.repeat('*', 3); + * // => '***' + * + * _.repeat('abc', 2); + * // => 'abcabc' + * + * _.repeat('abc', 0); + * // => '' + */ + function repeat(string, n, guard) { + if ((guard ? isIterateeCall(string, n, guard) : n === undefined)) { + n = 1; + } else { + n = toInteger(n); + } + return baseRepeat(toString(string), n); + } + + /** + * Replaces matches for `pattern` in `string` with `replacement`. + * + * **Note:** This method is based on + * [`String#replace`](https://mdn.io/String/replace). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to modify. + * @param {RegExp|string} pattern The pattern to replace. + * @param {Function|string} replacement The match replacement. + * @returns {string} Returns the modified string. + * @example + * + * _.replace('Hi Fred', 'Fred', 'Barney'); + * // => 'Hi Barney' + */ + function replace() { + var args = arguments, + string = toString(args[0]); + + return args.length < 3 ? string : string.replace(args[1], args[2]); + } + + /** + * Converts `string` to + * [snake case](https://en.wikipedia.org/wiki/Snake_case). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the snake cased string. + * @example + * + * _.snakeCase('Foo Bar'); + * // => 'foo_bar' + * + * _.snakeCase('fooBar'); + * // => 'foo_bar' + * + * _.snakeCase('--FOO-BAR--'); + * // => 'foo_bar' + */ + var snakeCase = createCompounder(function(result, word, index) { + return result + (index ? '_' : '') + word.toLowerCase(); + }); + + /** + * Splits `string` by `separator`. + * + * **Note:** This method is based on + * [`String#split`](https://mdn.io/String/split). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to split. + * @param {RegExp|string} separator The separator pattern to split by. + * @param {number} [limit] The length to truncate results to. + * @returns {Array} Returns the string segments. + * @example + * + * _.split('a-b-c', '-', 2); + * // => ['a', 'b'] + */ + function split(string, separator, limit) { + if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) { + separator = limit = undefined; + } + limit = limit === undefined ? MAX_ARRAY_LENGTH : limit >>> 0; + if (!limit) { + return []; + } + string = toString(string); + if (string && ( + typeof separator == 'string' || + (separator != null && !isRegExp(separator)) + )) { + separator = baseToString(separator); + if (!separator && hasUnicode(string)) { + return castSlice(stringToArray(string), 0, limit); + } + } + return string.split(separator, limit); + } + + /** + * Converts `string` to + * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). + * + * @static + * @memberOf _ + * @since 3.1.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the start cased string. + * @example + * + * _.startCase('--foo-bar--'); + * // => 'Foo Bar' + * + * _.startCase('fooBar'); + * // => 'Foo Bar' + * + * _.startCase('__FOO_BAR__'); + * // => 'FOO BAR' + */ + var startCase = createCompounder(function(result, word, index) { + return result + (index ? ' ' : '') + upperFirst(word); + }); + + /** + * Checks if `string` starts with the given target string. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to inspect. + * @param {string} [target] The string to search for. + * @param {number} [position=0] The position to search from. + * @returns {boolean} Returns `true` if `string` starts with `target`, + * else `false`. + * @example + * + * _.startsWith('abc', 'a'); + * // => true + * + * _.startsWith('abc', 'b'); + * // => false + * + * _.startsWith('abc', 'b', 1); + * // => true + */ + function startsWith(string, target, position) { + string = toString(string); + position = position == null + ? 0 + : baseClamp(toInteger(position), 0, string.length); + + target = baseToString(target); + return string.slice(position, position + target.length) == target; + } + + /** + * Creates a compiled template function that can interpolate data properties + * in "interpolate" delimiters, HTML-escape interpolated data properties in + * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data + * properties may be accessed as free variables in the template. If a setting + * object is given, it takes precedence over `_.templateSettings` values. + * + * **Note:** In the development build `_.template` utilizes + * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) + * for easier debugging. + * + * For more information on precompiling templates see + * [lodash's custom builds documentation](https://lodash.com/custom-builds). + * + * For more information on Chrome extension sandboxes see + * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category String + * @param {string} [string=''] The template string. + * @param {Object} [options={}] The options object. + * @param {RegExp} [options.escape=_.templateSettings.escape] + * The HTML "escape" delimiter. + * @param {RegExp} [options.evaluate=_.templateSettings.evaluate] + * The "evaluate" delimiter. + * @param {Object} [options.imports=_.templateSettings.imports] + * An object to import into the template as free variables. + * @param {RegExp} [options.interpolate=_.templateSettings.interpolate] + * The "interpolate" delimiter. + * @param {string} [options.sourceURL='lodash.templateSources[n]'] + * The sourceURL of the compiled template. + * @param {string} [options.variable='obj'] + * The data object variable name. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the compiled template function. + * @example + * + * // Use the "interpolate" delimiter to create a compiled template. + * var compiled = _.template('hello <%= user %>!'); + * compiled({ 'user': 'fred' }); + * // => 'hello fred!' + * + * // Use the HTML "escape" delimiter to escape data property values. + * var compiled = _.template('<%- value %>'); + * compiled({ 'value': ' + + + + - - - - - - - - - -
- Deliverable: - - Repo: - - +
Results
diff --git a/plugins/example/portal/frontend/html/admin.html b/plugins/example/portal/frontend/html/admin.html index 480b93d93..f5e7d3c5f 100644 --- a/plugins/example/portal/frontend/html/admin.html +++ b/plugins/example/portal/frontend/html/admin.html @@ -111,16 +111,16 @@