diff --git a/backend/package-lock.json b/backend/package-lock.json index 80d913f..630af31 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -4189,7 +4189,8 @@ "node_modules/aws4": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" }, "node_modules/axios": { "version": "1.7.8", diff --git a/backend/src/common/interfaces.ts b/backend/src/common/interfaces.ts index 5bf1b3b..cab439b 100644 --- a/backend/src/common/interfaces.ts +++ b/backend/src/common/interfaces.ts @@ -41,7 +41,6 @@ export interface PackageRating { BusFactor: number; ResponsiveMaintainer: number; LicenseScore: number; - GoodPinningPractice: number; PullRequest: number; NetScore: number; RampUpLatency: number; @@ -49,7 +48,6 @@ export interface PackageRating { BusFactorLatency: number; ResponsiveMaintainerLatency: number; LicenseScoreLatency: number; - GoodPinningPracticeLatency: number; PullRequestLatency: number; NetScoreLatency: number; } @@ -92,4 +90,4 @@ export interface PackageTableRow { JSProgram?: string; // JavaScript program for sensitive modules standaloneCost?: number; // Standalone cost, excluding dependencies totalCost?: number; // Total cost including dependencies if applicable -} \ No newline at end of file +} diff --git a/backend/src/common/utils.ts b/backend/src/common/utils.ts index 98b12e4..c7c271d 100644 --- a/backend/src/common/utils.ts +++ b/backend/src/common/utils.ts @@ -73,10 +73,10 @@ export async function getScores(token: string, url: string): Promise ResponsiveMaintainer_Latency: -1, License: -1, License_Latency: -1, - GoodPinningPractice: -1, - GoodPinningPracticeLatency: -1, PullRequest: -1, - PullRequestLatency: 0-1 + GoodPinningPractice: -1, + GoodPinningPractice_Latency: -1, + PullRequest_Latency: -1 }; console.log('Error calculating score:', error); return emptyResult; @@ -103,4 +103,4 @@ export async function getRepoUrl(url: string): Promise { export function extractFieldFromPackageJson(packageJson: string, field: string) { const metadata = JSON.parse(packageJson); return metadata[field]; -} \ No newline at end of file +} diff --git a/backend/src/handlers/PackageRate/interfaces.ts b/backend/src/handlers/PackageRate/interfaces.ts index 11901a7..92ef07b 100644 --- a/backend/src/handlers/PackageRate/interfaces.ts +++ b/backend/src/handlers/PackageRate/interfaces.ts @@ -92,4 +92,4 @@ export interface PackageTableRow { JSProgram?: string; // JavaScript program for sensitive modules standaloneCost: number; // Standalone cost, excluding dependencies Rating: PackageRating; -} \ No newline at end of file +} diff --git a/backend/src/services/rate/__tests__/gitAnalysis.test.ts b/backend/src/services/rate/__tests__/gitAnalysis.test.ts index f52f444..ef953e1 100644 --- a/backend/src/services/rate/__tests__/gitAnalysis.test.ts +++ b/backend/src/services/rate/__tests__/gitAnalysis.test.ts @@ -17,6 +17,7 @@ const fakeRepoData: repoData = { licenses: [], numberOfCommits: -1, numberOfLines: -1, + pullRequestMetrics: undefined, documentation: { hasReadme: false, numLines: -1, @@ -31,7 +32,8 @@ const fakeRepoData: repoData = { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; @@ -109,6 +111,80 @@ describe('gitAnalysisClass', () => { it('should have a gitData with values', async () => { const result = await gitAnalysisInstance.runTasks(url); expect(result).not.toBe(fakeRepoData); - }, 10000); + }, 30000); + + // In gitAnalysis.test.ts + + describe('Pull Request Analysis', () => { + it('should fetch pull request metrics', async () => { + await gitAnalysisInstance.fetchPullRequests(fakeRepoData); + expect(fakeRepoData.pullRequestMetrics).toBeDefined(); + if (fakeRepoData.pullRequestMetrics) { + expect(fakeRepoData.pullRequestMetrics.reviewedFraction).toBeGreaterThanOrEqual(0); + expect(fakeRepoData.pullRequestMetrics.reviewedFraction).toBeLessThanOrEqual(1); + expect(fakeRepoData.pullRequestMetrics.totalAdditions).toBeGreaterThanOrEqual(0); + expect(fakeRepoData.pullRequestMetrics.reviewedAdditions).toBeGreaterThanOrEqual(0); + } + }, 30000); + + it('should handle repositories with no access or invalid repos', async () => { + const invalidRepo = { + ...fakeRepoData, + repoUrl: 'https://github.com/definitely-not-real/non-existent-repo', + repoOwner: 'definitely-not-real', + repoName: 'non-existent-repo' + }; + + // Temporarily reduce exponential backoff for this test + const originalBackoff = gitAnalysisInstance.exponentialBackoff; + gitAnalysisInstance.exponentialBackoff = async (requestFn) => { + try { + return await requestFn(); + } catch (error) { + throw error; + } + }; + + await gitAnalysisInstance.fetchPullRequests(invalidRepo); + + // Restore original exponential backoff + gitAnalysisInstance.exponentialBackoff = originalBackoff; + + expect(invalidRepo.pullRequestMetrics).toEqual({ + totalAdditions: 0, + reviewedAdditions: 0, + reviewedFraction: 0 + }); + }, 10000); // Reduced timeout since we're bypassing backoff + + it('should handle API errors gracefully', async () => { + const badRepo = { + ...fakeRepoData, + repoOwner: '', // Invalid owner + repoName: '' // Invalid name + }; + + // Temporarily reduce exponential backoff for this test + const originalBackoff = gitAnalysisInstance.exponentialBackoff; + gitAnalysisInstance.exponentialBackoff = async (requestFn) => { + try { + return await requestFn(); + } catch (error) { + throw error; + } + }; + + await gitAnalysisInstance.fetchPullRequests(badRepo); + + // Restore original exponential backoff + gitAnalysisInstance.exponentialBackoff = originalBackoff; + + expect(badRepo.pullRequestMetrics).toEqual({ + totalAdditions: 0, + reviewedAdditions: 0, + reviewedFraction: 0 + }); + }, 10000); // Reduced timeout since we're bypassing backoff + }); }); diff --git a/backend/src/services/rate/__tests__/metricCalc.test.ts b/backend/src/services/rate/__tests__/metricCalc.test.ts index ca85b33..c096752 100644 --- a/backend/src/services/rate/__tests__/metricCalc.test.ts +++ b/backend/src/services/rate/__tests__/metricCalc.test.ts @@ -9,10 +9,15 @@ const fakeRepoData: repoData = { numberOfContributors: 400, numberOfOpenIssues: 10, numberOfClosedIssues: 20, - lastCommitDate: "Sat Mar 09 2024", + lastCommitDate: "Sat Nov 09 2024", licenses: [''], numberOfCommits: 1200, numberOfLines: 600, + pullRequestMetrics: { + totalAdditions: 1000, + reviewedAdditions: 800, + reviewedFraction: 0.8 + }, documentation: { hasReadme: true, numLines: 1000, @@ -27,7 +32,8 @@ const fakeRepoData: repoData = { licenses: 0, numberOfCommits: 0, numberOfLines: 0, - documentation: 0 + documentation: 0, + pullRequests: 0 } }; @@ -43,6 +49,7 @@ const invalidData: repoData = { licenses: [''], numberOfCommits: -1, numberOfLines: -1, + pullRequestMetrics: undefined, documentation: { hasReadme: false, numLines: -1, @@ -57,7 +64,8 @@ const invalidData: repoData = { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; @@ -241,8 +249,89 @@ describe('metricCalcClass', () => { expect(netScore).toEqual(0); }); + describe('calculatePinnedDependencies', () => { + it('should return 1.0 for a repo with no dependencies', () => { + fakeRepoData.dependencies = []; + const pinnedDependencies = metricClass.calculatePinnedDependencies(fakeRepoData); + expect(pinnedDependencies).toEqual(1.0); + }); + + it('should calculate the fraction of pinned dependencies correctly', () => { + fakeRepoData.dependencies = [ + { name: 'dep1', version: '1.0.0' }, + { name: 'dep2', version: '2.3.x' }, + { name: 'dep3', version: '^1.2.3' }, + ]; + const pinnedDependencies = metricClass.calculatePinnedDependencies(fakeRepoData); + expect(pinnedDependencies).toEqual(0.667); + }); + + it('should return 0 for invalid dependencies', () => { + fakeRepoData.dependencies = undefined; + const pinnedDependencies = metricClass.calculatePinnedDependencies(fakeRepoData); + expect(pinnedDependencies).toEqual(0); + }); + }); + + describe('Pull Request Metrics', () => { + beforeEach(() => { + // Reset fakeRepoData for each test + fakeRepoData.pullRequestMetrics = { + totalAdditions: 1000, + reviewedAdditions: 800, + reviewedFraction: 0.8 + }; + }); + + it('should calculate PR score correctly with valid metrics', () => { + const prScore = metricClass.calculatePullRequestScore(fakeRepoData); + expect(prScore).toEqual(0.8); + }); + + it('should return 0 for undefined PR metrics', () => { + fakeRepoData.pullRequestMetrics = undefined; + const prScore = metricClass.calculatePullRequestScore(fakeRepoData); + expect(prScore).toEqual(0); + }); + + it('should return 0 for zero total additions', () => { + fakeRepoData.pullRequestMetrics = { + totalAdditions: 0, + reviewedAdditions: 0, + reviewedFraction: 0 + }; + const prScore = metricClass.calculatePullRequestScore(fakeRepoData); + expect(prScore).toEqual(0); + }); + + it('should return correct latency value', () => { + fakeRepoData.latency.pullRequests = 2000; // 2 seconds in milliseconds + const latency = metricClass.getPullRequestLatency(fakeRepoData.latency); + expect(latency).toEqual(2.0); + }); + + it('should include PR score in net score calculation', () => { + // Set up a scenario where we know the expected output + fakeRepoData.licenses = ['MIT']; + fakeRepoData.pullRequestMetrics = { + totalAdditions: 1000, + reviewedAdditions: 1000, + reviewedFraction: 1.0 + }; + + const netScore = metricClass.calculateNetScore(fakeRepoData); + expect(netScore).toBeGreaterThan(0); + expect(netScore).toBeLessThanOrEqual(1); + }); + }); + // Test the overall getValue function it('Return correct values from getValue method', () => { + fakeRepoData.dependencies = [ + { name: 'dep1', version: '1.0.0' }, + { name: 'dep2', version: '2.3.x' }, + ]; + const result = metricClass.getValue(fakeRepoData); expect(result).toHaveProperty('URL', fakeRepoData.repoUrl); expect(result).toHaveProperty('NetScore'); @@ -251,5 +340,6 @@ describe('metricCalcClass', () => { expect(result).toHaveProperty('RampUp'); expect(result).toHaveProperty('ResponsiveMaintainer'); expect(result).toHaveProperty('License'); + expect(result).toHaveProperty('GoodPinningPractice'); }); -}); \ No newline at end of file +}); diff --git a/backend/src/services/rate/__tests__/npmAnalysis.test.ts b/backend/src/services/rate/__tests__/npmAnalysis.test.ts index c39253a..510d3e6 100644 --- a/backend/src/services/rate/__tests__/npmAnalysis.test.ts +++ b/backend/src/services/rate/__tests__/npmAnalysis.test.ts @@ -68,7 +68,8 @@ describe('npmAnalysis', () => { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; @@ -115,7 +116,8 @@ describe('npmAnalysis', () => { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; @@ -141,6 +143,55 @@ describe('npmAnalysis', () => { expect(mockLogger.logDebug).toHaveBeenCalledWith(`No commits found in the repository ${mockNpmData.repoUrl} in dir ./repo`); }); }); + describe('analyzeDependencies', () => { + it('should calculate pinned dependency fraction correctly', async () => { + const dir = './testRepo'; // Mock directory + const npmData: npmData = { + repoUrl: 'https://github.com/example/repo', + lastCommitDate: '', + documentation: { + hasReadme: true, + numLines: 100, + hasExamples: false, + hasDocumentation: true, + dependencies: undefined, // Initially undefined + }, + latency: { + contributors: -1, + openIssues: -1, + closedIssues: -1, + lastCommitDate: -1, + licenses: -1, + numberOfCommits: -1, + numberOfLines: -1, + documentation: -1, + dependencies: -1, + pullRequests: -1, + }, + }; + + // Mock reading of package.json + jest.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({ + dependencies: { + 'dep1': '1.0.0', + 'dep2': '2.3.x', + }, + devDependencies: { + 'dep3': '^1.2.3', + 'dep4': '1.2.5', + }, + })); + + const instance = new npmAnalysis(mockEnvVars); + await instance.analyzeDependencies(dir, npmData); + + expect(npmData.documentation.dependencies).toEqual({ + total: 4, + pinned: 3, + fractionPinned: 0.75, + }); + }); + }); describe('deleteRepo', () => { it('should delete the repository', async () => { @@ -160,4 +211,6 @@ describe('npmAnalysis', () => { expect(mockLogger.logDebug).toHaveBeenCalledWith('Failed to delete repository in ./repo:'); }); }); + + }); diff --git a/backend/src/services/rate/__tests__/scripts.test.ts b/backend/src/services/rate/__tests__/scripts.test.ts index 5d778da..bfefc18 100644 --- a/backend/src/services/rate/__tests__/scripts.test.ts +++ b/backend/src/services/rate/__tests__/scripts.test.ts @@ -4,7 +4,7 @@ import { envVars } from '../utils/interfaces'; import { getEnvVars } from '../tools/getEnvVars'; // Mock Data for Testing -const url = "https://github.com/phillips302/ECE461"; +const url = "https://github.com/axios/axios"; const fakeRepoData: repoData = { repoName: '', @@ -17,11 +17,21 @@ const fakeRepoData: repoData = { licenses: [], numberOfCommits: -1, numberOfLines: -1, + pullRequestMetrics: { + totalAdditions: 0, + reviewedAdditions: 0, + reviewedFraction: 0 + }, documentation: { hasReadme: false, numLines: -1, hasExamples: false, - hasDocumentation: false + hasDocumentation: false, + dependencies: { + total: -1, + fractionPinned: -1, + pinned: -1 + } }, latency: { contributors: -1, @@ -31,7 +41,8 @@ const fakeRepoData: repoData = { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; @@ -46,11 +57,17 @@ const fakeWrongRepoData: repoData = { licenses: [], numberOfCommits: -1, numberOfLines: -1, + pullRequestMetrics: undefined, documentation: { hasReadme: false, numLines: -1, hasExamples: false, - hasDocumentation: false + hasDocumentation: false, + dependencies: { + total: 0, + fractionPinned: 1.0, + pinned: 0 + } }, latency: { contributors: -1, @@ -60,7 +77,8 @@ const fakeWrongRepoData: repoData = { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; @@ -76,12 +94,22 @@ describe('runAnalysisClass', () => { // Test run analysis with good url it('should have a valid token', async () => { const result = await runAnalysisInstance.runAnalysis([url]); - expect(result).not.toBe([fakeRepoData]); - }, 15000); + expect(result).not.toEqual([fakeRepoData]); + }, 50000); // Test run analysis with bad url - it('should have a valid token', async () => { + it('have a valid token', async () => { const result = await runAnalysisInstance.runAnalysis(["https://pypi.org/"]); expect(result).toStrictEqual([fakeWrongRepoData]); }); -}); \ No newline at end of file + + it('should include dependency analysis results in the documentation field', async () => { + const result = await runAnalysisInstance.runAnalysis([url]); + console.log('Full result:', JSON.stringify(result, null, 2)); + console.log('Documentation object:', result[0]?.documentation); + console.log('Dependencies:', result[0]?.documentation?.dependencies); + + expect(result[0].documentation.dependencies).toBeDefined(); + expect(result[0].documentation.dependencies).toHaveProperty('fractionPinned'); + }, 50000); +}); diff --git a/backend/src/services/rate/index.ts b/backend/src/services/rate/index.ts index e45b4f6..918f97f 100644 --- a/backend/src/services/rate/index.ts +++ b/backend/src/services/rate/index.ts @@ -85,4 +85,4 @@ program } }); -program.parse(process.argv); \ No newline at end of file +program.parse(process.argv); diff --git a/backend/src/services/rate/tools/api.ts b/backend/src/services/rate/tools/api.ts index c059b1e..8c12bd4 100644 --- a/backend/src/services/rate/tools/api.ts +++ b/backend/src/services/rate/tools/api.ts @@ -93,7 +93,34 @@ export class npmAnalysis { this.logger.logDebug(`Error retrieving the last commit in ${dir} for ${npmData.repoUrl} from lastCommitDate`); } } - + + async analyzeDependencies(dir: string, npmData: npmData): Promise { + try { + const packageJsonPath = `${dir}/package.json`; + const packageJson = await fs.readFile(packageJsonPath, 'utf-8'); + const packageData = JSON.parse(packageJson); + + const dependencies = {... packageData.dependencies, ... packageData.devDependencies}; + const pinnedCount = Object.values(dependencies).filter(version => /^\d+\.\d+/.test(version as string)).length; + + const totalDependencies = Object.keys(dependencies).length; + const fractionPinned = totalDependencies > 0 ? pinnedCount / totalDependencies : 1.0; + + npmData.documentation.dependencies = { + total: totalDependencies, + pinned: pinnedCount, + fractionPinned: parseFloat(fractionPinned.toFixed(3)) + }; + } + catch (error) { + this.logger.logDebug(`Error analyzing dependencies in ${dir} for ${npmData.repoUrl}: ${error}`); + npmData.documentation.dependencies = { + total: 0, + pinned: 0, + fractionPinned: 1.0 + }; + } + } async deleteRepo(dir: string): Promise { this.logger.logDebug(`Deleting repository ${dir}...`); try { @@ -111,6 +138,7 @@ export class npmAnalysis { return endTime - startTime; } + // Main function to run the tasks in order async runTasks(url: string, dest: number, version: string): Promise { const repoDir = './dist/repoDir'+dest.toString(); @@ -123,6 +151,7 @@ export class npmAnalysis { numLines: -1, hasExamples: false, hasDocumentation: false, + dependencies: undefined, }, latency: { contributors: -1, @@ -132,7 +161,10 @@ export class npmAnalysis { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + dependencies: -1, + pullRequests: -1 + } }; @@ -143,6 +175,8 @@ export class npmAnalysis { this.executeTasks(this.lastCommitDate.bind(this), repoDir, npmData), this.executeTasks(this.getReadmeContent.bind(this), repoDir, npmData) ]); + + npmData.latency.dependencies = await this.executeTasks(this.analyzeDependencies.bind(this), repoDir, npmData); await this.deleteRepo(repoDir); this.logger.logInfo(`All npm tasks completed in order within dir ${repoDir}`); @@ -433,7 +467,88 @@ export class gitAnalysis { this.logger.logDebug(`Error fetching number of lines for ${gitData.repoName}`, error); } } + // Add to api.ts in gitAnalysis class + + async fetchPullRequests(gitData: gitData): Promise { + this.logger.logDebug(`Fetching pull requests for ${gitData.repoName}...`); + try { + // Validate owner and repo name first + if (!gitData.repoOwner || !gitData.repoName) { + throw new Error('Invalid repository owner or name'); + } + let page = 1; + let totalReviewedAdditions = 0; + let totalAdditions = 0; + let hasMorePRs = true; + + while (hasMorePRs) { + try { + // Fetch PRs with pagination + const response = await this.exponentialBackoff(() => + this.axiosInstance.get(`/repos/${gitData.repoOwner}/${gitData.repoName}/pulls`, { + params: { + state: 'closed', + per_page: 100, + page: page + } + }) + ); + + const prs = response.data; + hasMorePRs = prs.length === 100; + + // Process each PR + for (const pr of prs) { + try { + // Get PR details including additions + const prDetailResponse = await this.exponentialBackoff(() => + this.axiosInstance.get(`/repos/${gitData.repoOwner}/${gitData.repoName}/pulls/${pr.number}`) + ); + + const additions = prDetailResponse.data.additions; + totalAdditions += additions; + + // Get reviews for this PR + const reviewsResponse = await this.exponentialBackoff(() => + this.axiosInstance.get(`/repos/${gitData.repoOwner}/${gitData.repoName}/pulls/${pr.number}/reviews`) + ); + + // If PR has reviews, count its additions + if (reviewsResponse.data.length > 0) { + totalReviewedAdditions += additions; + } + } catch (prError) { + this.logger.logDebug(`Error processing PR ${pr.number}: ${prError}`); + continue; // Skip this PR and continue with others + } + } + + page++; + } catch (pageError) { + this.logger.logDebug(`Error fetching page ${page}: ${pageError}`); + break; // Stop processing if we can't fetch a page + } + } + + // Set the metrics with calculated values + gitData.pullRequestMetrics = { + totalAdditions, + reviewedAdditions: totalReviewedAdditions, + reviewedFraction: totalAdditions > 0 ? parseFloat((totalReviewedAdditions / totalAdditions).toFixed(3)) : 0 + }; + + this.logger.logDebug(`Pull request metrics calculated successfully for ${gitData.repoName}`); + } catch (error) { + this.logger.logDebug(`Error fetching pull requests for ${gitData.repoName}: ${error}`); + // Set default values in case of error + gitData.pullRequestMetrics = { + totalAdditions: 0, + reviewedAdditions: 0, + reviewedFraction: 0 + }; + } + } private async executeTasks(task: (gitData: gitData) => Promise, gitData: gitData): Promise { const startTime = performance.now(); await task(gitData); @@ -460,7 +575,8 @@ export class gitAnalysis { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; diff --git a/backend/src/services/rate/tools/metricCalc.ts b/backend/src/services/rate/tools/metricCalc.ts index 8c3ed9b..06dc0c6 100644 --- a/backend/src/services/rate/tools/metricCalc.ts +++ b/backend/src/services/rate/tools/metricCalc.ts @@ -114,16 +114,60 @@ export class metricCalc{ return 0; } + calculatePinnedDependencies(data: repoData): number { + //if invalid (undefined) dependencies are found + if(data.dependencies == undefined) { + return 0; + } + if (!data.dependencies || data.dependencies.length === 0) { + return 1.0; // Perfect score if no dependencies + } + + + + const pinnedCount = data.dependencies.filter(dep => /^\d+\.\d+/.test(dep.version)).length; + const fractionPinned = pinnedCount / data.dependencies.length; + + return parseFloat(fractionPinned.toFixed(3)); + } + + getPinnedDependenciesLatency(latency: repoLatencyData): number + { + if (latency.dependencies == undefined) { + return -1; + } + return parseFloat((latency.dependencies / 1000).toFixed(3)); + } + + calculatePullRequestScore(data: repoData): number { + if (!data.pullRequestMetrics) { + return 0; + } + + return data.pullRequestMetrics.reviewedFraction; + } + + getPullRequestLatency(latency: repoLatencyData): number { + return parseFloat((latency.pullRequests / 1000).toFixed(3)); + } + + + calculateNetScore(data: repoData): number { // Calculate the net score based on the individual metrics - const weightedScore = (0.3 * this.calculateResponsiveness(data)) + (0.25 * this.calculateCorrectness(data)) + (0.25 * this.calculateRampup(data)) + (0.2 * this.calculateBusFactor(data)); + const weightedScore = (0.25 * this.calculateResponsiveness(data)) + + (0.2 * this.calculateCorrectness(data)) + + (0.15 * this.calculateRampup(data)) + + (0.15 * this.calculateBusFactor(data))+ + (0.1 * this.calculatePinnedDependencies(data)) + + (0.15 * this.calculatePullRequestScore(data)); return this.checkLicenseExistence(data) * parseFloat(weightedScore.toFixed(3)); } getNetScoreLatency(latency: repoLatencyData): number { - return parseFloat(((Math.max(latency.openIssues, latency.licenses) + latency.numberOfLines + latency.closedIssues + latency.numberOfCommits + latency.contributors) / 1000).toFixed(3)); + return parseFloat(((Math.max(latency.openIssues, latency.licenses) + latency.numberOfLines + latency.closedIssues + latency.numberOfCommits + latency.contributors + latency.pullRequests + (latency.dependencies||0)) / 1000).toFixed(3)); //return parseFloat((Math.max(latency.numberOfLines, latency.openIssues, latency.closedIssues, latency.licenses, latency.numberOfCommits, latency.numberOfLines, latency.documentation) / 1000).toFixed(3)); } @@ -142,10 +186,10 @@ export class metricCalc{ ResponsiveMaintainer_Latency: parseFloat((data.latency.lastCommitDate / 1000).toFixed(3)), License: this.checkLicenseExistence(data), License_Latency: parseFloat((data.latency.licenses / 1000).toFixed(3)), - GoodPinningPractice: 0, - GoodPinningPracticeLatency: 0, - PullRequest: 0, - PullRequestLatency: 0 + GoodPinningPractice: this.calculatePinnedDependencies(data), + GoodPinningPractice_Latency: this.getPinnedDependenciesLatency(data.latency), + PullRequest: this.calculatePullRequestScore(data), + PullRequest_Latency: this.getPullRequestLatency(data.latency), }; } -} \ No newline at end of file +} diff --git a/backend/src/services/rate/tools/scripts.ts b/backend/src/services/rate/tools/scripts.ts index d7849ad..3c2744c 100644 --- a/backend/src/services/rate/tools/scripts.ts +++ b/backend/src/services/rate/tools/scripts.ts @@ -84,11 +84,17 @@ export class runAnalysis { licenses: [], numberOfCommits: -1, numberOfLines: -1, + pullRequestMetrics: undefined, documentation: { hasReadme: false, numLines: -1, hasExamples: false, - hasDocumentation: false + hasDocumentation: false, + dependencies: { + total: 0, + fractionPinned: 1.0, + pinned: 0, + } }, latency: { contributors: -1, @@ -98,7 +104,8 @@ export class runAnalysis { licenses: -1, numberOfCommits: -1, numberOfLines: -1, - documentation: -1 + documentation: -1, + pullRequests: -1 } }; @@ -124,11 +131,13 @@ export class runAnalysis { licenses: gitData.licenses, numberOfCommits: gitData.numberOfCommits, numberOfLines: gitData.numberOfLines, + pullRequestMetrics: gitData.pullRequestMetrics, documentation: { hasReadme: npmData.documentation.hasReadme, numLines: npmData.documentation.numLines, hasExamples: npmData.documentation.hasExamples, - hasDocumentation: npmData.documentation.hasDocumentation + hasDocumentation: npmData.documentation.hasDocumentation, + dependencies: npmData.documentation.dependencies }, latency: { contributors: gitData.latency.contributors, @@ -138,7 +147,9 @@ export class runAnalysis { licenses: gitData.latency.licenses, numberOfCommits: gitData.latency.numberOfCommits, numberOfLines: gitData.latency.numberOfLines, - documentation: npmData.latency.documentation + documentation: npmData.latency.documentation, + dependencies: npmData.latency.dependencies, + pullRequests: gitData.latency.pullRequests } }; diff --git a/backend/src/services/rate/utils/interfaces.ts b/backend/src/services/rate/utils/interfaces.ts index 44c40b0..3498e68 100644 --- a/backend/src/services/rate/utils/interfaces.ts +++ b/backend/src/services/rate/utils/interfaces.ts @@ -1,6 +1,15 @@ import { documentationData, repoLatencyData } from "./types"; +import { Dependency } from "./types"; +import { PullRequestMetrics } from "./types"; +export interface npmData { + repoUrl: string; + lastCommitDate: string; + documentation: documentationData; + latency: repoLatencyData; +} export interface repoData { + dependencies?: Dependency[]; repoName: string; repoUrl: string; repoOwner: string; @@ -13,6 +22,7 @@ export interface repoData { numberOfLines: number; documentation: documentationData; latency: repoLatencyData; + pullRequestMetrics?: PullRequestMetrics; } export interface gitData { @@ -26,6 +36,7 @@ export interface gitData { numberOfCommits: number; numberOfLines: number; latency: repoLatencyData; + pullRequestMetrics?: PullRequestMetrics; } export interface npmData { @@ -53,10 +64,10 @@ export interface metricData{ ResponsiveMaintainer_Latency: number; License: number; License_Latency: number; - GoodPinningPractice: number; - GoodPinningPracticeLatency: number; - PullRequest: number; - PullRequestLatency: number; NetScore: number; NetScore_Latency: number; -} \ No newline at end of file + GoodPinningPractice: number; + GoodPinningPractice_Latency: number; + PullRequest: number; + PullRequest_Latency: number; +} diff --git a/backend/src/services/rate/utils/types.ts b/backend/src/services/rate/utils/types.ts index 4256055..1524ec4 100644 --- a/backend/src/services/rate/utils/types.ts +++ b/backend/src/services/rate/utils/types.ts @@ -1,8 +1,24 @@ +export interface Dependency { + name: string; + version: string; +} + +export interface PullRequestMetrics { + totalAdditions: number; + reviewedAdditions: number; + reviewedFraction: number; +} + export type documentationData = { hasReadme: boolean; numLines: number; hasExamples: boolean; hasDocumentation: boolean; + dependencies?: { + total: number; + pinned: number; + fractionPinned: number; + }; }; export type repoLatencyData = { @@ -14,4 +30,6 @@ export type repoLatencyData = { numberOfCommits: number; numberOfLines: number; documentation: number; -} \ No newline at end of file + dependencies?: number; + pullRequests: number; +} diff --git a/backend/template.yml b/backend/template.yml index 33e2715..a5c3600 100644 --- a/backend/template.yml +++ b/backend/template.yml @@ -210,7 +210,7 @@ Resources: Runtime: nodejs18.x CodeUri: dist/handlers/PackageRetrieve MemorySize: 128 - Timeout: 10 + Timeout: 30 Layers: - !Ref DependenciesLayer Events: @@ -280,7 +280,7 @@ Resources: Runtime: nodejs18.x CodeUri: dist/handlers/PackageDelete MemorySize: 128 - Timeout: 3 + Timeout: 30 Layers: - !Ref DependenciesLayer Events: @@ -327,7 +327,7 @@ Resources: Runtime: nodejs18.x CodeUri: dist/handlers/PackageRate MemorySize: 128 - Timeout: 3 + Timeout: 30 Layers: - !Ref DependenciesLayer Events: @@ -386,7 +386,7 @@ Resources: Runtime: nodejs18.x CodeUri: dist/handlers/CreateAuthToken MemorySize: 128 - Timeout: 3 + Timeout: 30 Layers: - !Ref DependenciesLayer Events: @@ -404,7 +404,7 @@ Resources: Runtime: nodejs18.x CodeUri: dist/handlers/PackageByNameGet MemorySize: 128 - Timeout: 3 + Timeout: 30 Layers: - !Ref DependenciesLayer Events: @@ -465,7 +465,7 @@ Resources: Runtime: nodejs18.x CodeUri: dist/handlers/GetTracks MemorySize: 128 - Timeout: 3 + Timeout: 30 Layers: - !Ref DependenciesLayer Events: