Carlos Ble avatar Carlos Ble committed 7a5c712

first working version

Comments (0)

Files changed (6)

pyDoubles/.project

+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>pyDouble</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.python.pydev.PyDevBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.python.pydev.pythonNature</nature>
+	</natures>
+</projectDescription>

pyDoubles/.pydevproject

+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
+<path>/pyDouble/src</path>
+</pydev_pathproperty>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+</pydev_project>

pyDoubles/__init__.py

+import traceback
+
+ANY = "ANY"
+
+class CallsRegistry():
+    
+    def __init__(self):
+        self.calls = {}
+        
+    def _args(self, args):
+        return "args=" + str(args)
+    
+    def _kwargs(self, kwargs):
+        return "kwargs=" + str(kwargs)
+    
+    def register_call(self, method_name, *args, **kwargs):
+        if not self.calls.has_key(method_name):
+            self.calls[method_name] = []
+        self.calls[method_name].append(self._args(args))
+        self.calls[method_name].append(self._kwargs(kwargs))
+         
+    def was_registered(self, method_name):
+        return method_name in self.calls.keys()
+  
+    def _readable_kwargs(self, kwargs_str):
+        if kwargs_str == "{}":
+            return "No keyword args where passed in"
+        return kwargs_str
+      
+    def _format_err_msg(self, method_name, args_str, kwargs_str):
+        return "RECORDED calls were: << %s >>, \n EXPECTED call is << args = %s, keyword args = %s >>" % (
+                str(self.calls[method_name]), 
+                args_str, self._readable_kwargs(kwargs_str))
+
+    def match_call(self, method_name, *args, **kwargs):
+        if self._args(args)     not in self.calls[method_name] or \
+           self._kwargs(kwargs) not in self.calls[method_name]:
+                raise ArgsDontMatch(self._format_err_msg(
+                         method_name, str(args), str(kwargs)))
+        
+        
+   
+class StubsRepository():
+    def __init__(self):
+        self.stubs_return_values = {}
+        self.stubs_input_values = {}
+        self.str_input_args = "undefined_____"
+
+    def is_matching_stubbed_method(self, method_name, args, kwargs):
+       if self.stubs_return_values.has_key(method_name + str(args)):
+           return True
+       if self.stubs_return_values.has_key(method_name):
+           return True
+       return False
+
+    def return_value_given_input(self, method_name, args, kwargs):
+        if self.stubs_return_values.has_key(method_name):
+           return self.stubs_return_values[method_name]
+        else:
+           return self.stubs_return_values[method_name + str(args)]
+                      
+    def create_stub(self, method_name):
+        self.last_stubbed_method = method_name
+    
+    def set_input_for(self, args, kwargs):
+        self.stubs_input_values[self.last_stubbed_method + str(args)] = True
+        self.str_input_args = str(args)
+ 
+    def set_output_for_any_input(self, args):
+        self.stubs_input_values[self.last_stubbed_method] = ANY    
+        self.stubs_return_values[self.last_stubbed_method] = args
+            
+    def set_output_for(self, args):
+        self.stubs_return_values[self.last_stubbed_method + self.str_input_args] = args
+        
+    def must_stub_in_all_cases(self):
+        return not self.stubs_input_values.has_key(self.last_stubbed_method + self.str_input_args)
+        
+        
+                
+class ProxySpy():
+    """
+    This test double is just an interceptor to the original object.
+    It watches the calls to the original, recording what happens
+    so that we can make assertions on the calls that were made.
+    The actual methods in the original object are executed, they
+    are not mocked or stubbed by default
+    """
+    def __init__(self, original_instance):
+        self.registry = CallsRegistry()
+        self.original_instance = original_instance
+        self.original_instance._double = self
+        self.introspector = Introspector()
+        self.stubs = StubsRepository()
+
+    def __getattr__(self, attr):
+        if self.introspector.attr_was_invoked_as_method(attr):
+           self.invoked_method_name = attr
+           return self.interceptor
+        return getattr(self.original_instance, attr)
+    
+    def interceptor(self, *args, **kwargs):
+        self.registry.register_call(self.invoked_method_name, 
+                                 *args, **kwargs)
+        if self.stubs.is_matching_stubbed_method(
+            self.invoked_method_name, args, kwargs):
+                return self.stubs.return_value_given_input(
+                         self.invoked_method_name, args, kwargs)           
+        original_method = getattr(self.original_instance, 
+                                  self.invoked_method_name)
+        return original_method(*args, **kwargs)
+        
+    def was_called(self, method):
+        return self.registry.was_registered(
+                    self.introspector.method_name(method))
+
+    def with_args(self, *args, **kwargs):
+        if hasattr(self, "asserting_on_method"):
+           name = self.introspector.method_name(self.asserting_on_method)
+           self.registry.match_call(name, *args, **kwargs)
+        else:
+           self.stubs.set_input_for(args, kwargs)
+        return self
+        
+    def stub_out(self, method):
+        self.stubs.create_stub(self.introspector.method_name(method))
+        
+    def then_return(self, args):
+        if self.stubs.must_stub_in_all_cases():
+           self.stubs.set_output_for_any_input(args)
+        else:
+           self.stubs.set_output_for(args)
+
+
+
+class Introspector():
+    
+    def method_name(self, method):
+        return method.im_func.func_name
+    
+    def double_instance_from_method(self, method):
+        instance = method.im_self
+        if hasattr(instance, "_double"):
+            return instance._double
+        return instance
+    
+    def attr_was_invoked_as_method(self, attr):
+        for call in traceback.extract_stack():
+            if call[-1].find(attr + "(") != -1:
+               return True
+        return False    
+        
+
+def proxy_spy(obj_instance):
+    return ProxySpy(obj_instance)
+        
+def when(method):
+    double = Introspector().double_instance_from_method(method)
+    double.stub_out(method)
+    return double  
+        
+def assert_that_was_called(method):
+    try:
+        double = Introspector().double_instance_from_method(method) 
+        if double.was_called(method):
+           double.asserting_on_method = method
+           return double
+        raise UnexpectedBehavior("The method was not called")
+    except AttributeError, e:
+        raise WrongApiUsage(
+            "Make sure you call assert, passing in a method from a test double: (double.method)")
+        
+
+class UnexpectedBehavior(Exception):
+    pass
+
+class WrongApiUsage(Exception):
+    pass
+
+class ArgsDontMatch(Exception):
+    pass
+        

pyDoubles/docs.txt

+Documentation:
+
+RECORDED calls were << ['args = ()', 'kwargs= {'key_param': 'foo'}] >>, 
+ EXPECTED call has << args = ('bar',), keyword args = No keyword args where passed in >>
+
+This means that during the execution, the method
+was called like this: 
+
+some_method(key_param = 'foo')
+
+but the assertion made in the test was this:
+
+some_method('bar')
+
+keyword args are the arguments which have a name (optional parameters).
+
+If you want to return several values, use a list
+or tuple:
+when(self.rec.one_arg_method).then_return(1,2)
+
+when(self.rec.one_arg_method).then_return((1,2,3))
+
+
+More docs:
+http://hammingweight.com/UsingSpyObjects.pdf

pyDoublesTests/pyTDDmon.py

+#coding: utf-8
+from Tkinter import *
+import sys
+import os
+import glob
+
+from time import gmtime, strftime
+
+run_tests_script_file = 'pyTDDmon_tmp.py'
+
+def build_run_script(files):
+	header = 	'''\
+import unittest
+
+suite = unittest.TestSuite()
+load_module_tests = unittest.defaultTestLoader.loadTestsFromModule
+
+'''
+	middle = ""
+	for filename in files:
+		module = filename[:-3]
+		middle += 'import ' + module + '\n'
+		middle += 'suite.addTests(load_module_tests(' + module + '))\n\n'
+	footer = '''\
+if __name__ == '__main__':
+	unittest.TextTestRunner().run(suite)
+'''
+
+	return header + middle + footer
+
+def calculate_checksum(filelist, fileinfo):
+	val = 0
+	for f in filelist:
+		val += fileinfo.get_modified_time(f) + fileinfo.get_size(f)
+	return val
+
+class ColorPicker:
+	''' ColorPicker avgör bakgrundsfärgen i pyTDDmon-fönstret mha.
+	    antalet gröna tester, och totala antalet tester. Dessutom
+	    "pulserar" färgen mha API:t pulse(). '''
+	
+	def __init__(self):
+		self.color = 'green'
+		self.reset_pulse()
+
+	def pick(self):
+		return (self.light, self.color)
+
+	def pulse(self):
+		self.light = not self.light
+	
+	def reset_pulse(self):
+		self.light = True
+		
+	def set_result(self, (green, total)):
+		old_color = self.color
+		self.color = 'green'
+		if green == total-1:
+			self.color = 'red'
+		if green < total-1:
+			self.color = 'gray'
+		if self.color != old_color:
+			self.reset_pulse()
+						
+def win_text(total_tests, passing_tests=0, prev_total_tests=0):
+	if prev_total_tests > total_tests:
+		return "%d of %d tests green\n"% (passing_tests, total_tests) +\
+					 "Warning: number of tests decreased!" 
+	if total_tests == 0:
+		return "No tests found!"
+	if passing_tests == total_tests:
+		return "All %d tests green" % total_tests
+	txt = "%d of %d tests green"
+	if passing_tests+1 < total_tests:
+		txt = "Warning: only " + txt + "!"
+	return txt % (passing_tests, total_tests)
+
+class ScriptWriter:
+	'''
+	ScriptWriter: gets it's modules from the Finder, and
+	writes a test script using the FileWriter+script_builder
+	'''
+	def __init__(self, finder, file_writer, script_builder):
+		self.finder = finder
+		self.file_writer = file_writer
+		self.script_builder = script_builder
+		
+	def write_script(self):
+		result = self.script_builder.build_script_from_modules(self.finder.find_modules())
+		self.file_writer.write_file(run_tests_script_file, result)
+
+class TestScriptRunner:
+	''' TestScriptRunner - 
+	  Collaborators:
+       CmdRunner, runs a specified command line, returns stderr as string
+       Analyzer, analyses unittest-output into green,total number of tests
+	'''
+	def __init__(self, cmdrunner, analyzer):
+		self.cmdrunner = cmdrunner
+		self.analyzer = analyzer
+		
+	def run(self, test_script):
+		output = self.cmdrunner.run_cmdline('python '+test_script)
+		return self.analyzer.analyze(output)
+		
+class Analyzer:
+	'''
+	Analyzer
+	Analyserar unittest-output efter gröna test, och antal test.
+	Medarbetare: Log, dit loggmeddelande skrivs.
+	'''
+	def __init__(self, logger):
+		self.logger = logger
+		
+	def analyze(self, txt):
+		if len(txt.strip()) == 0:
+			return (0, 0)
+		toprow =  txt.splitlines()[0]
+		green = toprow.count('.')
+		total = len(toprow)
+		if green<total:
+			self.logger.log(txt)
+		return (green, total)
+
+class Logger:
+	''' Logger, samlar ihop loggmeddelanden till en lång sträng. '''
+	
+	def __init__(self):
+		self.clear()
+		
+	def log(self, message):
+		self.complete_log = self.complete_log + message
+		
+	def get_log(self):
+		return self.complete_log
+		
+	def clear(self):
+		self.complete_log = ""
+		
+## Rows above this are unit-tested.
+## Rows below this are not unit-tested.
+
+class RealFileInfo:
+	def get_size(self, f):
+		return os.stat(f).st_size
+	def get_modified_time(self, f):
+		return os.stat(f).st_mtime
+
+class Finder:
+	def find_modules(self):
+		return glob.glob("test_*.py")
+	
+class FinderWithFixedFileSet:
+	def __init__(self, files):
+		self.files = files
+	
+	def find_modules(self):
+		return self.files
+		
+def safe_remove(path):
+	try: os.unlink(path)
+	except: pass
+
+class CmdRunner:
+	def run_cmdline(self, cmdline):
+		os.system(cmdline + " 2>tmp2.txt")
+		try:
+			f = open('tmp2.txt', "r")
+			output = f.read()
+		finally:
+			f.close()
+		safe_remove('tmp2.txt')
+		return output
+		
+class FileWriter:
+	def write_file(self, filename, content):
+		f = open(filename, 'w')
+		f.write(content)
+		f.close()
+		
+class ScriptBuilder:
+	def build_script_from_modules(self, modules):
+		return build_run_script(modules)
+
+def message_window(message):
+	win = Toplevel()
+	win.title('Log')
+	def destroy(something):
+		win.destroy()
+	white = '#ffffff'
+	label=Label(win, text=message, bg=white, activebackground=white)
+	label.pack()
+	label.bind("<Button-2>", destroy)
+	label.bind("<Button-3>", destroy)
+
+class pyTDDmonFrame(Frame):
+
+	def __init__(self, files=None):
+		Frame.__init__(self, None)
+		self.grid()
+		self.create_button()
+		self.failures = 0
+		self.last_checksum = 0
+		self.run()
+		self.num_tests = 0
+		self.num_tests_diff = 0
+		self.logger = Logger()
+		self.color_picker = ColorPicker()
+		self.runner = TestScriptRunner(CmdRunner(), Analyzer(self.logger))
+		finder = Finder()
+		if files != None:
+			finder = FinderWithFixedFileSet(files)
+		self.script_writer = ScriptWriter(finder, FileWriter(), ScriptBuilder())
+		self.color_table = {
+			(True, 'green'): '0f0',
+			(False, 'green'): '0c0',
+			(True, 'red'): 'f00',
+			(False, 'red'): 'c00',
+			(True, 'gray'): '999',
+			(False, 'gray'): '555'
+		}
+				
+	def compute_checksum(self):
+		files = glob.glob('*.py')
+		try: files.remove(run_tests_script_file)
+		except: pass
+		return calculate_checksum(files, RealFileInfo())
+
+	def get_number_of_failures(self):
+		self.script_writer.write_script()
+		(green, total) = self.runner.run(run_tests_script_file)
+		self.num_tests_prev = self.num_tests
+		self.num_tests = total
+		return total - green
+
+	def clock_string(self):
+		return strftime("%H:%M:%S", gmtime())
+
+	def create_button(self):
+		self.button = Label(self, text = 'pyTDDmon')
+		self.button.bind("<Button-1>", self.button_clicked)
+		self.button.bind("<Button-2>", self.end_program)
+		self.button.bind("<Button-3>", self.end_program)
+		self.button.grid()
+
+	def button_clicked(self, widget):
+		message_window(self.logger.get_log())
+
+	def end_program(self, widget):
+		safe_remove(run_tests_script_file)
+		self.quit()
+
+	def run(self):
+		print("")
+		print(" _______________________________________")
+		print("|       pyTDDmon window opened          |")
+		print("|_______________________________________|")
+		print("|                                       |")
+		print("| Left click pyTDDmon: show test output |")
+		print('| Right click  - " - : quit             |')
+		print('|_______________________________________|')
+
+	def update_gui(self):
+		(green, total, prev_total) = (self.num_tests-self.failures, self.num_tests, self.num_tests_prev)
+		self.update_gui_color(green, total)
+		self.update_gui_text(green, total, prev_total)
+	
+	def update_gui_color(self, green, total):
+		self.color_picker.set_result( (green, total) )
+		(light, color) = self.color_picker.pick()
+ 		self.color_picker.pulse()
+		rgb = '#' + self.color_table[(light, color)]
+		self.button.configure(bg=rgb, activebackground=rgb)
+		
+	def update_gui_text(self, green, total, prev_total):
+		txt = "pyTDDmon\n" + win_text(passing_tests = green, total_tests = total, prev_total_tests = prev_total)
+		self.button.configure(text=txt)
+
+	def look_for_changes(self):
+		newval = self.compute_checksum()
+		if newval != self.last_checksum:
+			self.last_checksum = newval
+			self.logger.clear()
+			self.logger.log('[%s] Running all tests...\n' % self.clock_string())
+			self.failures = self.get_number_of_failures()
+			self.logger.log('[%s] Number of failures: %d\n' % (self.clock_string(), self.failures))
+		self.update_gui()
+		self.after(750, self.look_for_changes)
+
+def file_exists(f):
+	try:
+		o = open(f, "r")
+		o.close()
+	except:
+		print(f + " does not exist")
+		return False
+	print(f + " exists")
+	return True
+
+def filter_existing_files(files):
+	return [f for f in files if file_exists(f)]
+
+if __name__ == '__main__':
+	filtered = filter_existing_files(sys.argv[1:])
+	if len(filtered)>0:
+		app = pyTDDmonFrame(filtered)
+	else:
+		app = pyTDDmonFrame()
+	app.master.title(" ")
+	app.master.resizable(0,0)
+	app.look_for_changes()
+	try:
+		app.mainloop() 
+	except:
+		pass

pyDoublesTests/unit.py

+# -*- coding: utf-8 -*-
+
+import unittest
+from pyDoubles import *
+
+class SimpleObject():
+    """
+    The original object we double in tests
+    """
+    def hello(self):
+        return "hello"
+    
+    def something(self):
+        return "ok"
+    
+    def one_arg_method(self, arg1):
+        return arg1
+    
+    def more_than_one_argument(self, arg1, arg2):
+        return arg1 + arg2
+    
+    def keyword_argument(self, key_param=False):
+        return key_param
+    
+    def mixed_method(self, arg1, key_param=False):
+        return key_param + arg1
+
+class RecorderTests(unittest.TestCase):
+    
+    def setUp(self):
+        self.spy = proxy_spy(SimpleObject())
+        
+    def test_assert_was_called(self):
+        self.spy.hello()
+        
+        assert_that_was_called(self.spy.hello)
+
+    def test_assert_was_called_on_any_method(self):
+        self.spy.something()
+        
+        assert_that_was_called(self.spy.something)
+
+    def test_assert_needs_always_a_method_from_a_double(self):
+        self.failUnlessRaises(WrongApiUsage,
+                assert_that_was_called, self.spy)
+
+    def test_assert_needs_always_a_method_from_a_double_not_the_original(self):
+        self.failUnlessRaises(WrongApiUsage,
+             assert_that_was_called, SimpleObject().hello)
+
+    def test_one_method_called_other_wasnt(self):
+        self.spy.something()
+        
+        self.failUnlessRaises(UnexpectedBehavior, 
+             assert_that_was_called, self.spy.hello)                
+
+    def test_two_methods_called_assert_on_the_first(self):
+        self.spy.hello()
+        self.spy.something()
+        
+        assert_that_was_called(self.spy.hello)
+
+    def test_get_method_name(self):
+        name = Introspector().method_name(self.spy.hello)
+        
+        self.assertEquals("hello", name)
+                
+    def test_call_original_method(self):
+        self.assertEquals("ok", self.spy.something())
+
+    def test_get_instance_from_method(self):
+        rec_found = Introspector().double_instance_from_method(self.spy.hello)
+        
+        self.assertEquals(self.spy, rec_found)
+                
+    def test_assert_was_called_when_wasnt(self):
+        self.failUnlessRaises(UnexpectedBehavior, 
+             assert_that_was_called, self.spy.hello)                
+
+    def test_was_called_with_same_parameters(self):
+        self.spy.one_arg_method(1)
+        
+        assert_that_was_called(self.spy.one_arg_method).with_args(1)
+
+    def test_was_called_with_same_parameters_in_variables(self):
+        arg1 = 1
+        self.spy.one_arg_method(arg1)
+        
+        assert_that_was_called(self.spy.one_arg_method).with_args(1)
+    
+    def test_was_called_with_same_parameters_when_not(self):
+        self.spy.one_arg_method(1)
+        args_checker = assert_that_was_called(self.spy.one_arg_method)
+        
+        self.failUnlessRaises(ArgsDontMatch,
+            args_checker.with_args, 2)
+        
+    def test_get_args_of_last_call(self):
+        self.spy.one_arg_method(1)
+        
+        self.spy.registry.match_call("one_arg_method", 1)
+    
+    def test_was_called_with_same_params_but_no_params_accepted(self):
+        self.spy.hello()
+        args_checker = assert_that_was_called(self.spy.hello)
+        
+        self.failUnlessRaises(ArgsDontMatch,
+            args_checker.with_args, "something")
+
+    def test_was_called_with_several_parameters(self):
+        self.spy.more_than_one_argument(1, 2)
+        args_checker = assert_that_was_called(self.spy.more_than_one_argument)
+       
+        args_checker.with_args(1, 2) 
+
+    def test_was_called_with_parameters_not_matching(self):
+        self.spy.one_arg_method(1)
+        args_checker = assert_that_was_called(self.spy.one_arg_method)
+
+        self.failUnlessRaises(ArgsDontMatch,
+            args_checker.with_args, "2") 
+
+    def test_was_called_with_keyed_args_not_matching(self):
+        self.spy.keyword_argument(key_param="foo")
+        args_checker = assert_that_was_called(self.spy.keyword_argument)
+
+        self.failUnlessRaises(ArgsDontMatch,
+            args_checker.with_args, key_param="bar")
+
+    def test_was_called_with_keyed_args_matching(self):
+        self.spy.keyword_argument(key_param="foo")
+        assert_that_was_called(self.spy.keyword_argument).with_args(
+                                    key_param="foo")
+        
+    def test_recorded_call_params_are_displayed(self):
+        self.spy.keyword_argument(key_param="foo")
+        try:
+            assert_that_was_called(self.spy.keyword_argument
+                                   ).with_args("bar")
+        except Exception, e:
+            self.assertTrue(str(e).find("foo") != -1, str(e))
+
+    def test_stub_out_method(self):
+        when(self.spy.one_arg_method).then_return(3)
+        
+        self.assertEquals(3, self.spy.one_arg_method(5))
+        
+        assert_that_was_called(self.spy.one_arg_method).with_args(5)
+
+    def test_stub_out_method_returning_a_list(self):
+        when(self.spy.one_arg_method).then_return([1,2,3])
+        
+        self.assertEquals([1,2,3], self.spy.one_arg_method(5))
+        
+        assert_that_was_called(self.spy.one_arg_method).with_args(5)
+
+    def test_stub_out_method_with_args(self):
+        when(self.spy.one_arg_method).with_args(2).then_return(3)
+        
+        self.assertEquals(3, self.spy.one_arg_method(2))
+        
+        assert_that_was_called(self.spy.one_arg_method).with_args(2)
+
+    def test_stub_out_method_with_args_calls_actual(self):
+        when(self.spy.one_arg_method).with_args(2).then_return(3)
+        
+        self.assertEquals(4, self.spy.one_arg_method(4))
+        
+        assert_that_was_called(self.spy.one_arg_method).with_args(4)
+
+
+    def test_stub_out_method_with_several_inputs(self):
+        when(self.spy.one_arg_method).with_args(2).then_return(3)
+        when(self.spy.one_arg_method).with_args(3).then_return(4)
+        
+        self.assertEquals(3, self.spy.one_arg_method(2))
+        self.assertEquals(4, self.spy.one_arg_method(3))
+
+    def test_recorded_calls_work_on_several_stubs(self):
+        when(self.spy.one_arg_method).with_args(2).then_return(3)
+        when(self.spy.one_arg_method).with_args(3).then_return(4)
+        
+        self.spy.one_arg_method(2)        
+        self.spy.one_arg_method(3)
+        assert_that_was_called(self.spy.one_arg_method).with_args(2)
+        assert_that_was_called(self.spy.one_arg_method).with_args(3)
+
+class CallsRegistryTests(unittest.TestCase):
+    
+    def test_match_call_once_its_been_registered(self):
+        registry = CallsRegistry()
+        registry.register_call("methodX", 1,2,3)
+        registry.match_call("methodX", 1,2,3)
+        
+            
+        
+        
+
+if __name__ == "__main__":
+    print "Use nosetest to run this tests: nosetest unit.py"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.