Skip to content

Commit

Permalink
feat: add tests for dashboard script (#3344)
Browse files Browse the repository at this point in the history
Co-authored-by: Ansh Goyal <[email protected]>
  • Loading branch information
vishvamsinh28 and anshgoyalevil authored Nov 7, 2024
1 parent 8e34ea8 commit d9f5cdc
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 17 deletions.
44 changes: 27 additions & 17 deletions scripts/dashboard/build-dashboard.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { writeFileSync } = require('fs');
const { writeFile } = require('fs-extra');
const { resolve } = require('path');
const { graphql } = require('@octokit/graphql');
const { Queries } = require('./issue-queries');
Expand Down Expand Up @@ -44,10 +44,10 @@ async function getDiscussions(query, pageSize, endCursor = null) {
return result.search.nodes.concat(await getDiscussions(query, pageSize, result.search.pageInfo.endCursor));
} catch (e) {
console.error(e);

return Promise.reject(e);
}
}

async function getDiscussionByID(isPR, id) {
try {
const result = await graphql(isPR ? Queries.pullRequestById : Queries.issueById, {
Expand All @@ -60,7 +60,6 @@ async function getDiscussionByID(isPR, id) {
return result;
} catch (e) {
console.error(e);

return Promise.reject(e);
}
}
Expand All @@ -69,7 +68,6 @@ async function processHotDiscussions(batch) {
return Promise.all(
batch.map(async (discussion) => {
try {
// eslint-disable-next-line no-underscore-dangle
const isPR = discussion.__typename === 'PullRequest';
if (discussion.comments.pageInfo.hasNextPage) {
const fetchedDiscussion = await getDiscussionByID(isPR, discussion.id);
Expand All @@ -83,9 +81,10 @@ async function processHotDiscussions(batch) {

const finalInteractionsCount = isPR
? interactionsCount +
discussion.reviews.totalCount +
discussion.reviews.nodes.reduce((acc, curr) => acc + curr.comments.totalCount, 0)
discussion.reviews.totalCount +
discussion.reviews.nodes.reduce((acc, curr) => acc + curr.comments.totalCount, 0)
: interactionsCount;

return {
id: discussion.id,
isPR,
Expand All @@ -98,7 +97,7 @@ async function processHotDiscussions(batch) {
score: finalInteractionsCount / (monthsSince(discussion.timelineItems.updatedAt) + 2) ** 1.8
};
} catch (e) {
console.error(`there was some issues while parsing this item: ${JSON.stringify(discussion)}`);
console.error(`there were some issues while parsing this item: ${JSON.stringify(discussion)}`);
throw e;
}
})
Expand All @@ -111,21 +110,28 @@ async function getHotDiscussions(discussions) {

for (let i = 0; i < discussions.length; i += batchSize) {
const batch = discussions.slice(i, i + batchSize);
// eslint-disable-next-line no-await-in-loop
const batchResults = await processHotDiscussions(batch);

// eslint-disable-next-line no-await-in-loop
await pause(1000);

result.push(...batchResults);
}

result.sort((ElemA, ElemB) => ElemB.score - ElemA.score);
const filteredResult = result.filter((issue) => issue.author !== 'asyncapi-bot');
return filteredResult.slice(0, 12);
}
async function writeToFile(content) {
writeFileSync(resolve(__dirname, '..', '..', 'dashboard.json'), JSON.stringify(content, null, ' '));

async function writeToFile(content, writePath) {
try {
await writeFile(writePath, JSON.stringify(content, null, ' '));
} catch (error) {
console.error('Failed to write dashboard data:', {
error: error.message,
writePath
});
throw error;
}
}

async function mapGoodFirstIssues(issues) {
return issues.map((issue) => ({
id: issue.id,
Expand Down Expand Up @@ -153,7 +159,7 @@ function monthsSince(date) {
return Math.floor(months);
}

async function start() {
async function start(writePath) {
try {
const issues = await getDiscussions(Queries.hotDiscussionsIssues, 20);
const PRs = await getDiscussions(Queries.hotDiscussionsPullRequests, 20);
Expand All @@ -163,12 +169,16 @@ async function start() {
getHotDiscussions(discussions),
mapGoodFirstIssues(rawGoodFirstIssues)
]);
writeToFile({ hotDiscussions, goodFirstIssues });
return await writeToFile({ hotDiscussions, goodFirstIssues }, writePath);
} catch (e) {
console.log('There were some issues parsing data from github.');
console.log(e);
}
}
start();

module.exports = { getLabel, monthsSince, mapGoodFirstIssues, getHotDiscussions, getDiscussionByID };
/* istanbul ignore next */
if (require.main === module) {
start(resolve(__dirname, '..', '..', 'dashboard.json'));
}

module.exports = { getLabel, monthsSince, mapGoodFirstIssues, getHotDiscussions, getDiscussionByID, getDiscussions, writeToFile, start, processHotDiscussions };
198 changes: 198 additions & 0 deletions tests/dashboard/build-dashboard.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
const { graphql } = require('@octokit/graphql');
const { promises: fs, mkdirSync, rmSync } = require('fs-extra');
const { resolve } = require('path');
const os = require('os');
const {
getLabel,
monthsSince,
mapGoodFirstIssues,
getHotDiscussions,
getDiscussionByID,
writeToFile,
getDiscussions,
start
} = require('../../scripts/dashboard/build-dashboard');

const {
issues,
mockDiscussion,
discussionWithMoreComments,
fullDiscussionDetails,
mockRateLimitResponse
} = require("../fixtures/dashboardData")

jest.mock('@octokit/graphql');

describe('GitHub Discussions Processing', () => {
let tempDir;
let consoleErrorSpy;
let consoleLogSpy;

beforeAll(() => {
tempDir = resolve(os.tmpdir(), 'test-config');
mkdirSync(tempDir);
});

afterAll(() => {
rmSync(tempDir, { recursive: true, force: true });
});

beforeEach(() => {
jest.clearAllMocks();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
});

afterEach(() => {
consoleErrorSpy.mockRestore();
consoleLogSpy.mockRestore();
});

it('should fetch additional discussion details when comments have next page', async () => {
graphql.mockResolvedValueOnce(fullDiscussionDetails);

const result = await getHotDiscussions([discussionWithMoreComments]);

expect(graphql).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
id: 'paginated-discussion',
headers: expect.any(Object)
})
);

expect(result[0]).toMatchObject({
id: 'paginated-discussion',
isPR: false,
title: 'Test with Pagination'
});

const firstResult = result[0];
expect(firstResult.score).toBeGreaterThan(0);
});

it('should handle rate limit warnings', async () => {
graphql.mockResolvedValueOnce(mockRateLimitResponse);

await getDiscussions('test-query', 10);

expect(consoleLogSpy).toHaveBeenCalledWith(
'[WARNING] GitHub GraphQL rateLimit',
'cost = 1',
'limit = 5000',
'remaining = 50',
expect.any(String)
);
});

it('should handle pagination', async () => {
const mockFirstResponse = {
search: {
nodes: [mockDiscussion],
pageInfo: { hasNextPage: true, endCursor: 'cursor1' }
},
rateLimit: { remaining: 1000 }
};

const mockSecondResponse = {
search: {
nodes: [{ ...mockDiscussion, id: 'test-id-2' }],
pageInfo: { hasNextPage: false }
},
rateLimit: { remaining: 1000 }
};

graphql
.mockResolvedValueOnce(mockFirstResponse)
.mockResolvedValueOnce(mockSecondResponse);

const result = await getDiscussions('test-query', 10);
expect(result).toHaveLength(2);
});

it('should handle complete failure', async () => {
graphql.mockRejectedValue(new Error('Complete API failure'));

const filePath = resolve(tempDir, 'error-output.json');
await start(filePath);

expect(consoleLogSpy).toHaveBeenCalledWith('There were some issues parsing data from github.');
});

it('should successfully process and write data', async () => {
graphql.mockResolvedValue(mockRateLimitResponse);

const filePath = resolve(tempDir, 'success-output.json');
await start(filePath);

const content = JSON.parse(await fs.readFile(filePath, 'utf-8'));
expect(content).toHaveProperty('hotDiscussions');
expect(content).toHaveProperty('goodFirstIssues');
});

it('should get labels correctly', () => {
const issue = {
labels: { nodes: [{ name: 'area/bug' }, { name: 'good first issue' }] }
};
expect(getLabel(issue, 'area/')).toBe('bug');
expect(getLabel(issue, 'nonexistent/')).toBeUndefined();
});

it('should calculate months since date', () => {
const date = new Date();
date.setMonth(date.getMonth() - 2);
expect(monthsSince(date)).toBe(2);
});

it('should map good first issues', async () => {

const result = await mapGoodFirstIssues(issues);
expect(result[0]).toMatchObject({
id: '1',
area: 'docs'
});
});

it('should handle discussion retrieval', async () => {
graphql.mockResolvedValueOnce({ node: mockDiscussion });
const result = await getDiscussionByID(false, 'test-id');
expect(result.node).toBeDefined();

graphql.mockRejectedValueOnce(new Error('API error'));
await expect(getDiscussionByID(true, 'test-id')).rejects.toThrow();
});

it('should process hot discussions', async () => {
const prDiscussion = {
...mockDiscussion,
__typename: 'PullRequest',
reviews: {
totalCount: 1,
nodes: [{ comments: { totalCount: 1 } }]
}
};

const result = await getHotDiscussions([mockDiscussion, prDiscussion]);
expect(result.length).toBeLessThanOrEqual(12);
});

it('should write to file', async () => {
const filePath = resolve(tempDir, 'test.json');
await writeToFile({ test: true }, filePath);
const content = JSON.parse(await fs.readFile(filePath, 'utf-8'));
expect(content).toEqual({ test: true });
});

it('should handle parsing errors in processHotDiscussions', async () => {
const localConsoleErrorSpy = jest.spyOn(console, 'error');

await expect(getHotDiscussions([undefined])).rejects.toThrow();

expect(consoleErrorSpy).toHaveBeenCalledWith(
'there were some issues while parsing this item: undefined'
);

localConsoleErrorSpy.mockRestore();
});

});
Loading

0 comments on commit d9f5cdc

Please sign in to comment.