Skip to content

Commit 9a2b5b5

Browse files
authored
Merge pull request #1234 from tsg-ut/test/startup-smoke-test-vitest
test: 起動時クラッシュを検知するスモークテストを追加
2 parents a1b8f8c + 99dc62e commit 9a2b5b5

11 files changed

Lines changed: 272 additions & 119 deletions

File tree

__mocks__/axios.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const {PassThrough} = require('stream');
22

3-
const axios = vi.fn((options = {}) => {
3+
const axios = vi.fn((urlOrConfig = {}, config = {}) => {
4+
// axios.get(url, config) / axios.post(url, data, config) のように、第一引数が
5+
// URL文字列で呼ばれるケースと、axios(config) のように設定オブジェクト単体で
6+
// 呼ばれるケースの両方に対応する。
7+
const options = typeof urlOrConfig === 'string' ? config : urlOrConfig;
48
if (options.responseType === 'stream') {
59
const stream = new PassThrough();
610
process.nextTick(() => {
@@ -13,9 +17,23 @@ const axios = vi.fn((options = {}) => {
1317
});
1418
axios.get = axios;
1519
axios.post = axios;
20+
axios.head = axios;
1621

1722
axios.response = '';
1823

24+
// axios.create() で作られたインスタンスは axios.defaults.* を直接
25+
// 読み書きできる必要がある(例: clientCH.defaults.withCredentials = false)。
26+
axios.defaults = {
27+
headers: {
28+
post: {},
29+
common: {},
30+
},
31+
};
32+
33+
// axios.create() はconfigをbindした新しいaxiosインスタンスを返すのが実際の
34+
// 挙動だが、このモックでは単純に同じモックを返す。
35+
axios.create = () => axios;
36+
1937
axios.default = {
2038
defaults: {},
2139
};

__mocks__/sqlite.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ const sqlite = {};
22

33
sqlite.open = vi.fn(() => ({
44
all: vi.fn(() => Promise.resolve(sqlite.records)),
5-
get: vi.fn(() => Promise.resolve(sqlite.records.length >= 1 ? sqlite.records[0] : null)),
5+
// 実際の sqlite/sqlite3 driver は該当行が無い場合 undefined を返す(null ではない)。
6+
get: vi.fn(() => Promise.resolve(sqlite.records.length >= 1 ? sqlite.records[0] : undefined)),
7+
run: vi.fn(() => Promise.resolve()),
8+
close: vi.fn(() => Promise.resolve()),
69
}));
710

811
sqlite.records = [];

atcoder/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,6 @@ export default async ({eventClient, webClient: slack}: SlackInterface) => {
656656
updateContests();
657657
});
658658
}, 30 * 60 * 1000);
659-
updateContests();
660659

661660
let time = Date.now();
662661
setInterval(() => {

emoxpand/index.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import logger from '../lib/logger';
66
/* eslint-disable no-unused-vars */
77
import type {SlackInterface, SlashCommandEndpoint} from '../lib/slack';
88
import {getMemberName, getMemberIcon} from '../lib/slackUtils';
9+
import {Loader} from '../lib/utils';
910

1011
const log = logger.child({bot: 'emoxpand'});
1112

@@ -39,20 +40,24 @@ const logError = (err: Error, mesg: string): void => {
3940
const emojiData = 'bigemojis.json';
4041
const emojiPath = path.resolve(__dirname, emojiData);
4142

42-
let allEmojis : EmojiTable = new Map();
43-
44-
const loadEmojis = async () => {
45-
allEmojis = new Map();
46-
const data = await fs.readFile(emojiPath, {encoding: 'utf8'});
47-
const obj: {[key: string]: EmojiContent} = JSON.parse(data);
48-
log.info('emoxpand: loading big emojis...');
49-
for (const [name, content] of Object.entries(obj)) {
50-
allEmojis.set(name, emojiFromContent(content));
43+
const emojisLoader = new Loader<EmojiTable>(async () => {
44+
const emojis: EmojiTable = new Map();
45+
try {
46+
const data = await fs.readFile(emojiPath, {encoding: 'utf8'});
47+
const obj: {[key: string]: EmojiContent} = JSON.parse(data);
48+
log.info('emoxpand: loading big emojis...');
49+
for (const [name, content] of Object.entries(obj)) {
50+
emojis.set(name, emojiFromContent(content));
51+
}
52+
} catch (err: unknown) {
53+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
54+
logError(err, 'failed to load big emojis');
55+
} else {
56+
throw err;
57+
}
5158
}
52-
return allEmojis;
53-
};
54-
55-
loadEmojis();
59+
return emojis;
60+
});
5661

5762
const emojisToJson = (emojis: EmojiTable): string => {
5863
const obj = Object.fromEntries(
@@ -66,7 +71,8 @@ const storeEmojis = (emojis: EmojiTable): void => {
6671
};
6772

6873

69-
const addEmoji = (name: EmojiName, emoji: BigEmoji): void => {
74+
const addEmoji = async (name: EmojiName, emoji: BigEmoji): Promise<void> => {
75+
const allEmojis = await emojisLoader.load();
7076
allEmojis.set(name, emoji);
7177
storeEmojis(allEmojis);
7278
};
@@ -97,7 +103,8 @@ interface Token {
97103
}
98104

99105

100-
const expandEmoji = (text: string): string => {
106+
const expandEmoji = async (text: string): Promise<string> => {
107+
const allEmojis = await emojisLoader.load();
101108
let replacementCount = 0;
102109
const result = _.flatMap(text.split('\n'), (line) => {
103110
const [, , tokens] = (line + '\n').split('').reduce<[TokenType, string[], Token[]]>(
@@ -112,7 +119,8 @@ const expandEmoji = (text: string): string => {
112119
kind: 'Plain',
113120
content: `!${content}!`,
114121
});
115-
} else {
122+
}
123+
else {
116124
parsed.push({
117125
kind: parsing,
118126
content: chars.join(''),
@@ -208,7 +216,7 @@ export const server = ({eventClient, webClient: slack}: SlackInterface) => plugi
208216
}
209217
slack.chat.postMessage({
210218
channel: request.body.channel_id,
211-
text: expandEmoji(request.body.text),
219+
text: await expandEmoji(request.body.text),
212220
username: await getMemberName(request.body.user_id),
213221
icon_url: await getMemberIcon(request.body.user_id, 192),
214222
});
@@ -217,9 +225,10 @@ export const server = ({eventClient, webClient: slack}: SlackInterface) => plugi
217225
// }}}
218226

219227
// Emoji list API {{{
220-
fastify.get('/emoxpand/list', (_, response) => {
228+
fastify.get('/emoxpand/list', async (_, response) => {
221229
response.header('Content-Type', 'applicaton/json').code(200);
222-
return Promise.resolve(emojisToJson(allEmojis));
230+
const allEmojis = await emojisLoader.load();
231+
return emojisToJson(allEmojis);
223232
});
224233
// }}}
225234

@@ -240,6 +249,7 @@ export const server = ({eventClient, webClient: slack}: SlackInterface) => plugi
240249

241250
// Big Emoji list {{{
242251
if (/^(?:|emoji)$/.test(message.text)) {
252+
const allEmojis = await emojisLoader.load();
243253
const emojiNames = Array.from(allEmojis.keys());
244254
if (emojiNames.length === 0) {
245255
postMessage('登録されている大絵文字はありません');
@@ -287,7 +297,7 @@ export const server = ({eventClient, webClient: slack}: SlackInterface) => plugi
287297
.split('\n')
288298
.map((row) =>
289299
row.slice(1, row.length - 1).split('::'));
290-
addEmoji(state.name, emojiFromContent(contentLines));
300+
await addEmoji(state.name, emojiFromContent(contentLines));
291301
postMessage(`大絵文字 \`!${state.name}!\`
292302
${match.groups.content}
293303
が登録されました:sushi-go-left::waiwai::saikou::chian-ga-aru::sushi-go-right:`);

index.ts

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { inspect } from 'util';
2424
import concat from 'concat-stream';
2525
import { getAuthorityLabel } from './lib/slackUtils';
2626
import { closeDuplicateEventChecker } from './lib/eventDeduplication';
27+
import { productionBots, allBots } from './lib/bots';
2728

2829
const log = logger.child({ bot: 'index' });
2930

@@ -69,93 +70,6 @@ process.on('SIGUSR2', () => gracefulShutdown('SIGUSR2'));
6970
// pm2 reload
7071
process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
7172

72-
const productionBots = [
73-
'summary',
74-
'mahjong',
75-
'pocky',
76-
'emoji-notifier',
77-
'sushi-bot',
78-
'shogi',
79-
'tiobot',
80-
'checkin',
81-
'tahoiya',
82-
'channel-notifier',
83-
'prime',
84-
'dajare',
85-
'sunrise',
86-
'ahokusa',
87-
// ...(word2vecInstalled ? ['vocabwar'] : []),
88-
'ricochet-robots',
89-
'scrapbox',
90-
'slack-log',
91-
'welcome',
92-
'deploy',
93-
'achievements',
94-
'mail-hook',
95-
'wordhero',
96-
'wordhero/crossword',
97-
'oauth',
98-
'tunnel',
99-
'voiperrobot',
100-
'atcoder',
101-
'lyrics',
102-
'better-custom-response',
103-
'emoxpand',
104-
'ponpe',
105-
'anime',
106-
'anime/anison',
107-
'oogiri',
108-
'sorting-riddles',
109-
'tsglive',
110-
'emoji-modifier',
111-
'context-free',
112-
'room-gacha',
113-
'taiko',
114-
'hayaoshi',
115-
'twitter-dm-notifier',
116-
'hitandblow',
117-
'discord',
118-
'octas',
119-
'pwnyaa',
120-
'amongyou',
121-
'api',
122-
'hangman',
123-
'hakatashi-visor',
124-
'nojoin',
125-
'remember-english',
126-
'golfbot',
127-
'kirafan/quiz',
128-
'topic',
129-
'bungo-quiz',
130-
'adventar',
131-
'jantama',
132-
'tabi-gatcha',
133-
'achievement-quiz',
134-
'wadokaichin',
135-
'wordle-battle',
136-
// 'slow-quiz',
137-
'dicebot',
138-
'taimai',
139-
'map-guessr',
140-
'character-quiz',
141-
'shmug',
142-
'pilot',
143-
'qrcode-quiz',
144-
'oneiromancy',
145-
'auto-archiver',
146-
'city-symbol',
147-
'nmpz',
148-
'autogen-quiz',
149-
'twenty-questions',
150-
'google-calendar',
151-
];
152-
153-
const developmentBots = [
154-
'helloworld',
155-
];
156-
157-
const allBots = [...productionBots, ...developmentBots];
158-
15973
log.info('slackbot started');
16074

16175
const argv = yargs

lib/__mocks__/getReading.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
const {vi} = require('vitest');
2-
31
const getReading = vi.fn((text) => (
42
Promise.resolve(getReading.virtualReadings.hasOwnProperty(text) ? getReading.virtualReadings[text] : '')
53
));

lib/__mocks__/state.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ const State: StateInterface = class State<StateObj> {
5151
export default State;
5252

5353
export const ReadOnlyState = State;
54+
55+
// lib/readOnlyState.ts は非production環境で StateDevelopment を
56+
// ReadOnlyState として使う。このモックにも同じ名前でexportしておく。
57+
export const StateDevelopment = State;

lib/bots.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
export const productionBots = [
2+
'summary',
3+
'mahjong',
4+
'pocky',
5+
'emoji-notifier',
6+
'sushi-bot',
7+
'shogi',
8+
'tiobot',
9+
'checkin',
10+
'tahoiya',
11+
'channel-notifier',
12+
'prime',
13+
'dajare',
14+
'sunrise',
15+
'ahokusa',
16+
// ...(word2vecInstalled ? ['vocabwar'] : []),
17+
'ricochet-robots',
18+
'scrapbox',
19+
'slack-log',
20+
'welcome',
21+
'deploy',
22+
'achievements',
23+
'mail-hook',
24+
'wordhero',
25+
'wordhero/crossword',
26+
'oauth',
27+
'tunnel',
28+
'voiperrobot',
29+
'atcoder',
30+
'lyrics',
31+
'better-custom-response',
32+
'emoxpand',
33+
'ponpe',
34+
'anime',
35+
'anime/anison',
36+
'oogiri',
37+
'sorting-riddles',
38+
'tsglive',
39+
'emoji-modifier',
40+
'context-free',
41+
'room-gacha',
42+
'taiko',
43+
'hayaoshi',
44+
'twitter-dm-notifier',
45+
'hitandblow',
46+
'discord',
47+
'octas',
48+
'pwnyaa',
49+
'amongyou',
50+
'api',
51+
'hangman',
52+
'hakatashi-visor',
53+
'nojoin',
54+
'remember-english',
55+
'golfbot',
56+
'kirafan/quiz',
57+
'topic',
58+
'bungo-quiz',
59+
'adventar',
60+
'jantama',
61+
'tabi-gatcha',
62+
'achievement-quiz',
63+
'wadokaichin',
64+
'wordle-battle',
65+
// 'slow-quiz',
66+
'dicebot',
67+
'taimai',
68+
'map-guessr',
69+
'character-quiz',
70+
'shmug',
71+
'pilot',
72+
'qrcode-quiz',
73+
'oneiromancy',
74+
'auto-archiver',
75+
'city-symbol',
76+
'nmpz',
77+
'autogen-quiz',
78+
'twenty-questions',
79+
'google-calendar',
80+
];
81+
82+
export const developmentBots = [
83+
'helloworld',
84+
];
85+
86+
export const allBots = [...productionBots, ...developmentBots];

lib/slackMock.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ export default class SlackMock extends EventEmitter implements SlackInterface {
169169
if (stack.join('.') === "users.list") {
170170
return Promise.resolve({ok: true, members: []});
171171
}
172+
if (stack.join('.') === "team.info") {
173+
return Promise.resolve({ok: true, team: {id: this.fakeTeam, name: 'fakeTeam'}});
174+
}
172175
if (stack.join('.') === "conversations.list") {
173176
return Promise.resolve({ok: true, channels: [
174177
{id: 'CGENERAL', is_general: true},

0 commit comments

Comments
 (0)