logger.spec.ts 12 KB

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