logger.spec.ts 12 KB

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