logger.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. // Copyright (c) Jupyter Development Team.
  2. import 'jest';
  3. // Distributed under the terms of the Modified BSD License.
  4. import {
  5. Logger,
  6. LoggerOutputAreaModel,
  7. ILogPayload,
  8. LogLevel
  9. } from '@jupyterlab/logconsole';
  10. import { RenderMimeRegistry } from '@jupyterlab/rendermime';
  11. import { Signal, ISignal } from '@lumino/signaling';
  12. class SignalLogger<SENDER, ARGS> {
  13. constructor(signal: ISignal<SENDER, ARGS>) {
  14. signal.connect(this.slot, this);
  15. }
  16. slot(sender: SENDER, args: ARGS) {
  17. this.args.push(args);
  18. }
  19. dispose() {
  20. Signal.disconnectAll(this);
  21. }
  22. args: ARGS[] = [];
  23. }
  24. describe('LoggerOutputAreaModel', () => {
  25. let model: LoggerOutputAreaModel;
  26. beforeEach(() => {
  27. model = new LoggerOutputAreaModel({ maxLength: 10 });
  28. });
  29. afterEach(() => {
  30. model.dispose();
  31. });
  32. describe('#constructor()', () => {
  33. it('should create an LoggerOutputAreaModel', () => {
  34. expect(model).toBeInstanceOf(LoggerOutputAreaModel);
  35. });
  36. it('should set the max length', async () => {
  37. const model = new LoggerOutputAreaModel({ maxLength: 10 });
  38. expect(model.maxLength).toEqual(10);
  39. model.dispose();
  40. });
  41. });
  42. describe('#maxLength', () => {
  43. it('should set the maximum number of messages in the first-in first-out queue', () => {
  44. for (let i = 0; i < 12; i++) {
  45. model.add({
  46. output_type: 'display_data',
  47. data: { 'text/plain': i.toString() },
  48. timestamp: Date.now(),
  49. level: 'info'
  50. });
  51. }
  52. expect(model.length).toEqual(10);
  53. expect(model.get(0).data['text/plain']).toEqual('2');
  54. });
  55. it('setting maxLength should immediately apply and trim the message list', () => {
  56. for (let i = 0; i < 12; i++) {
  57. model.add({
  58. output_type: 'display_data',
  59. data: { 'text/plain': i.toString() },
  60. timestamp: Date.now(),
  61. level: 'info'
  62. });
  63. }
  64. expect(model.maxLength).toEqual(10);
  65. expect(model.length).toEqual(10);
  66. model.maxLength = 5;
  67. expect(model.maxLength).toEqual(5);
  68. expect(model.length).toEqual(5);
  69. expect(model.get(0).data['text/plain']).toEqual('7');
  70. });
  71. });
  72. });
  73. describe('Logger', () => {
  74. let logger: Logger;
  75. beforeEach(() => {
  76. logger = new Logger({ source: 'test source', maxLength: 10 });
  77. });
  78. afterEach(() => {
  79. logger.dispose();
  80. });
  81. describe('#constructor()', () => {
  82. it('should create a Logger with initial properties', () => {
  83. expect(logger).toBeInstanceOf(Logger);
  84. expect(logger.source).toEqual('test source');
  85. expect(logger.maxLength).toEqual(10);
  86. });
  87. });
  88. describe('#maxLength', () => {
  89. it('should set the maximum number of messages in the first-in first-out queue', () => {
  90. for (let i = 0; i < 12; i++) {
  91. logger.log({ type: 'text', data: i.toString(), level: 'critical' });
  92. }
  93. expect(logger.length).toEqual(10);
  94. expect(logger.outputAreaModel.get(0).data['text/plain']).toEqual('2');
  95. });
  96. it('setting maxLength should immediately apply and trim the message list', () => {
  97. for (let i = 0; i < 12; i++) {
  98. logger.log({ type: 'text', data: i.toString(), level: 'critical' });
  99. }
  100. const model = logger.outputAreaModel;
  101. expect(logger.maxLength).toEqual(10);
  102. expect(logger.length).toEqual(10);
  103. logger.maxLength = 5;
  104. expect(logger.maxLength).toEqual(5);
  105. expect(logger.length).toEqual(5);
  106. expect(model.get(0).data['text/plain']).toEqual('7');
  107. });
  108. });
  109. describe('#level', () => {
  110. const levels: LogLevel[] = [
  111. 'critical',
  112. 'error',
  113. 'warning',
  114. 'info',
  115. 'debug'
  116. ];
  117. it('should default to "warning"', () => {
  118. expect(logger.level).toEqual('warning');
  119. });
  120. it.each(levels)('filters for messages: %s', (level: LogLevel) => {
  121. logger.level = level;
  122. const messages: ILogPayload[] = levels.map(level => ({
  123. type: 'text',
  124. data: level,
  125. level
  126. }));
  127. messages.forEach(m => logger.log({ ...m }));
  128. const logged: string[] = [];
  129. for (let i = 0; i < logger.length; i++) {
  130. const msg = logger.outputAreaModel.get(i);
  131. logged.push(msg.level);
  132. }
  133. const shouldInclude = levels.slice(0, levels.indexOf(level) + 1);
  134. const shouldExclude = levels.slice(levels.indexOf(level) + 1);
  135. shouldInclude.forEach(x => {
  136. expect(logged).toContain(x);
  137. });
  138. shouldExclude.forEach(x => {
  139. expect(logged).not.toContain(x);
  140. });
  141. });
  142. it('logs a "metadata" level text message if changed', () => {
  143. logger.level = 'info';
  144. const msg = logger.outputAreaModel.get(0);
  145. expect(msg.level).toBe('metadata');
  146. expect(msg.data['text/plain']).toContain('info');
  147. });
  148. it('emits a stateChanged signal when changing', () => {
  149. const s = new SignalLogger(logger.stateChanged);
  150. logger.level = 'info';
  151. expect(s.args).toEqual([
  152. {
  153. name: 'level',
  154. oldValue: 'warning',
  155. newValue: 'info'
  156. }
  157. ]);
  158. s.dispose();
  159. });
  160. it('setting to its current value has no effect', () => {
  161. const s = new SignalLogger(logger.stateChanged);
  162. logger.level = logger.level; // eslint-disable-line
  163. expect(s.args.length).toBe(0);
  164. expect(logger.length).toBe(0);
  165. s.dispose();
  166. });
  167. });
  168. describe('#length', () => {
  169. it('records how many messages are stored', () => {
  170. logger.log({ type: 'text', data: 'message 1', level: 'warning' });
  171. logger.log({ type: 'text', data: 'message 2', level: 'warning' });
  172. expect(logger.length).toBe(2);
  173. logger.clear();
  174. expect(logger.length).toBe(0);
  175. });
  176. it('may be less than the messages logged if messages were combined', () => {
  177. logger.log({
  178. type: 'output',
  179. data: { output_type: 'stream', name: 'stdout', text: 'message 1' },
  180. level: 'critical'
  181. });
  182. logger.log({
  183. type: 'output',
  184. data: { output_type: 'stream', name: 'stdout', text: 'message 2' },
  185. level: 'critical'
  186. });
  187. expect(logger.length).toBe(1);
  188. });
  189. });
  190. describe('#rendermime', () => {
  191. it('initially is null', () => {
  192. expect(logger.rendermime).toBe(null);
  193. });
  194. it('sets the rendermime attribute', () => {
  195. const value = new RenderMimeRegistry();
  196. logger.rendermime = value;
  197. expect(logger.rendermime).toBe(value);
  198. });
  199. it('emits a stateChanged signal when changed', () => {
  200. const oldValue = (logger.rendermime = new RenderMimeRegistry());
  201. const newValue = oldValue.clone();
  202. const s = new SignalLogger(logger.stateChanged);
  203. logger.rendermime = newValue;
  204. expect(s.args).toEqual([{ name: 'rendermime', oldValue, newValue }]);
  205. s.dispose();
  206. });
  207. it('setting to current value has no effect', () => {
  208. logger.rendermime = new RenderMimeRegistry();
  209. const s = new SignalLogger(logger.stateChanged);
  210. logger.rendermime = logger.rendermime; // eslint-disable-line
  211. expect(s.args).toEqual([]);
  212. s.dispose();
  213. });
  214. });
  215. describe('#version', () => {
  216. it('starts at zero', () => {
  217. expect(logger.version).toBe(0);
  218. });
  219. it('increments every time a message is logged', () => {
  220. logger.log({ type: 'text', data: 'message 1', level: 'warning' });
  221. logger.log({ type: 'text', data: 'message 2', level: 'warning' });
  222. expect(logger.version).toBe(2);
  223. });
  224. it('increments even if messages are combined', () => {
  225. logger.log({
  226. type: 'output',
  227. data: { output_type: 'stream', name: 'stdout', text: 'message 1' },
  228. level: 'critical'
  229. });
  230. logger.log({
  231. type: 'output',
  232. data: { output_type: 'stream', name: 'stdout', text: 'message 2' },
  233. level: 'critical'
  234. });
  235. expect(logger.length).toBe(1);
  236. expect(logger.version).toBe(2);
  237. });
  238. it('does not increment on clearing messages', () => {
  239. logger.log({ type: 'text', data: 'message 1', level: 'warning' });
  240. logger.log({ type: 'text', data: 'message 2', level: 'warning' });
  241. expect(logger.version).toBe(2);
  242. logger.clear();
  243. expect(logger.length).toBe(0);
  244. expect(logger.version).toBe(2);
  245. });
  246. });
  247. describe('#log()', () => {
  248. it('logs text messages', () => {
  249. logger.log({ type: 'text', data: 'message', level: 'warning' });
  250. expect(logger.length).toBe(1);
  251. });
  252. it('logs html messages', () => {
  253. logger.log({ type: 'html', data: 'message', level: 'warning' });
  254. expect(logger.length).toBe(1);
  255. });
  256. it('logs output stream messages', () => {
  257. logger.log({
  258. type: 'output',
  259. data: { output_type: 'stream', name: 'stdout', text: 'message' },
  260. level: 'warning'
  261. });
  262. expect(logger.length).toBe(1);
  263. });
  264. it('logs display_data messages', () => {
  265. logger.log({
  266. type: 'output',
  267. data: {
  268. output_type: 'display_data',
  269. data: { 'text/plain': 'message' }
  270. },
  271. level: 'warning'
  272. });
  273. expect(logger.length).toBe(1);
  274. });
  275. it('logs execute_result messages', () => {
  276. logger.log({
  277. type: 'output',
  278. data: {
  279. output_type: 'execute_result',
  280. data: { 'text/plain': 'message', execution_count: 5 }
  281. },
  282. level: 'warning'
  283. });
  284. expect(logger.length).toBe(1);
  285. });
  286. it('logs error messages', () => {
  287. logger.log({
  288. type: 'output',
  289. data: {
  290. output_type: 'error',
  291. ename: 'Error',
  292. evalue: 'Error',
  293. traceback: ['level 1', 'level 2']
  294. },
  295. level: 'warning'
  296. });
  297. expect(logger.length).toBe(1);
  298. });
  299. it('emits an "append" content changed signal', () => {
  300. const s = new SignalLogger(logger.contentChanged);
  301. logger.log({ type: 'text', data: 'message 1', level: 'warning' });
  302. expect(s.args).toEqual(['append']);
  303. s.dispose();
  304. });
  305. it('emits an "append" content changed signal', () => {
  306. const s = new SignalLogger(logger.contentChanged);
  307. logger.log({
  308. type: 'output',
  309. data: { output_type: 'stream', name: 'stdout', text: 'message 1' },
  310. level: 'critical'
  311. });
  312. logger.log({
  313. type: 'output',
  314. data: { output_type: 'stream', name: 'stdout', text: 'message 2' },
  315. level: 'critical'
  316. });
  317. expect(s.args).toEqual(['append', 'append']);
  318. expect(logger.length).toBe(1);
  319. s.dispose();
  320. });
  321. it('adds a timestamp to the message', () => {
  322. const before = Date.now();
  323. logger.log({ type: 'text', data: 'message 1', level: 'warning' });
  324. const after = Date.now();
  325. const msgTime = logger.outputAreaModel.get(0).timestamp.getTime();
  326. expect(msgTime).toBeGreaterThanOrEqual(before);
  327. expect(msgTime).toBeLessThanOrEqual(after);
  328. });
  329. });
  330. describe('#clear()', () => {
  331. it('clears messages', () => {
  332. logger.log({ type: 'text', data: 'message 1', level: 'warning' });
  333. logger.log({ type: 'text', data: 'message 2', level: 'warning' });
  334. expect(logger.length).toBe(2);
  335. logger.clear();
  336. expect(logger.length).toBe(0);
  337. });
  338. it('emits a "clear" content changed signal', () => {
  339. const s = new SignalLogger(logger.contentChanged);
  340. logger.log({ type: 'text', data: 'message 1', level: 'warning' });
  341. logger.clear();
  342. expect(s.args).toEqual(['append', 'clear']);
  343. s.dispose();
  344. });
  345. });
  346. describe('#checkpoint()', () => {
  347. it('adds a metadata message to the message list', () => {
  348. logger.checkpoint();
  349. expect(logger.outputAreaModel.get(0).level).toBe('metadata');
  350. });
  351. it('emits an "append" content changed signal', () => {
  352. const s = new SignalLogger(logger.contentChanged);
  353. logger.checkpoint();
  354. expect(s.args).toEqual(['append']);
  355. s.dispose();
  356. });
  357. });
  358. });